diff options
author | Grzegorz Bizon <grzesiek.bizon@gmail.com> | 2018-08-21 11:12:23 +0200 |
---|---|---|
committer | Grzegorz Bizon <grzesiek.bizon@gmail.com> | 2018-08-21 11:12:23 +0200 |
commit | 6232c26ac7aa279f7082ceb04b16e42673196475 (patch) | |
tree | 04ec906c8f55528e4735be86de76da4f3c1fd520 | |
parent | 00aa4d4c8c57a2f305070c2f9cb86fde6bc14285 (diff) | |
parent | cc0bf2f6231a3c231450a7ef8f272de9f9591eaa (diff) | |
download | gitlab-ce-6232c26ac7aa279f7082ceb04b16e42673196475.tar.gz |
Merge branch 'master' into sh-add-object-storage-qa
* master: (31 commits)
131 files changed, 1767 insertions, 778 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index fd02d72b4c2..797a20ef16e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -739,7 +739,7 @@ karma: - chrome_debug.log - coverage-javascript/ -codequality: +code_quality: <<: *dedicated-no-docs-no-db-pull-cache-job image: docker:stable allow_failure: true @@ -757,9 +757,13 @@ codequality: script: # Extract "MAJOR.MINOR" from CI_SERVER_VERSION and generate "MAJOR-MINOR-stable" for Security Products - export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/') - - docker run --env SOURCE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock "registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code + - docker run + --env SOURCE_CODE="$PWD" + --volume "$PWD":/code + --volume /var/run/docker.sock:/var/run/docker.sock + "registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code artifacts: - paths: [codeclimate.json] + paths: [gl-code-quality-report.json] expire_in: 1 week sast: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e68e3b9cab0..fb7c0c88629 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -64,11 +64,11 @@ As of July 2018, all the documentation for contributing to the GitLab project ha ## Contribute to GitLab -For a first-time step-by-step guide to the contribution process, see -["Contributing to GitLab"](https://about.gitlab.com/contributing/). - Thank you for your interest in contributing to GitLab. This guide details how -to contribute to GitLab in a way that is efficient for everyone. +to contribute to GitLab in a way that is easy for everyone. + +For a first-time step-by-step guide to the contribution process, please see +["Contributing to GitLab"](https://about.gitlab.com/contributing/). Looking for something to work on? Look for issues with the label [Accepting Merge Requests](#i-want-to-contribute). @@ -77,10 +77,10 @@ source edition, and GitLab Enterprise Edition (EE) which is our commercial edition. Throughout this guide you will see references to CE and EE for abbreviation. -If you have read this guide and want to know how the GitLab [core team] +If you want to know how the GitLab [core team] operates please see [the GitLab contributing process](PROCESS.md). -- [GitLab Inc engineers should refer to the engineering workflow document](https://about.gitlab.com/handbook/engineering/workflow/) +[GitLab Inc engineers should refer to the engineering workflow document](https://about.gitlab.com/handbook/engineering/workflow/) ## Security vulnerability disclosure diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 90bdef2ea8c..377d8aca07e 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -0.117.1 +0.117.2 @@ -214,9 +214,6 @@ gem 'jira-ruby', '~> 1.4' # Flowdock integration gem 'gitlab-flowdock-git-hook', '~> 1.0.1' -# Gemnasium integration -gem 'gemnasium-gitlab-service', '~> 0.2' - # Slack integration gem 'slack-notifier', '~> 1.5.1' @@ -423,7 +420,7 @@ group :ed25519 do end # Gitaly GRPC client -gem 'gitaly-proto', '~> 0.112.0', require: 'gitaly' +gem 'gitaly-proto', '~> 0.113.0', require: 'gitaly' gem 'grpc', '~> 1.11.0' # Locked until https://github.com/google/protobuf/issues/4210 is closed diff --git a/Gemfile.lock b/Gemfile.lock index 1aadc3fd0b6..15a105579fb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -269,8 +269,6 @@ GEM fuubar (2.2.0) rspec-core (~> 3.0) ruby-progressbar (~> 1.4) - gemnasium-gitlab-service (0.2.6) - rugged (~> 0.21) gemojione (3.3.0) json get_process_mem (0.2.0) @@ -284,7 +282,7 @@ GEM gettext_i18n_rails (>= 0.7.1) po_to_json (>= 1.0.0) rails (>= 3.2.0) - gitaly-proto (0.112.0) + gitaly-proto (0.113.0) google-protobuf (~> 3.1) grpc (~> 1.10) github-linguist (5.3.3) @@ -1040,12 +1038,11 @@ DEPENDENCIES font-awesome-rails (~> 4.7) foreman (~> 0.84.0) fuubar (~> 2.2.0) - gemnasium-gitlab-service (~> 0.2) gemojione (~> 3.3) gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.3) - gitaly-proto (~> 0.112.0) + gitaly-proto (~> 0.113.0) github-linguist (~> 5.3.3) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-gollum-lib (~> 4.2) diff --git a/Gemfile.rails5.lock b/Gemfile.rails5.lock index af70e2c1939..7803d12c6b4 100644 --- a/Gemfile.rails5.lock +++ b/Gemfile.rails5.lock @@ -272,8 +272,6 @@ GEM fuubar (2.2.0) rspec-core (~> 3.0) ruby-progressbar (~> 1.4) - gemnasium-gitlab-service (0.2.6) - rugged (~> 0.21) gemojione (3.3.0) json get_process_mem (0.2.0) @@ -287,7 +285,7 @@ GEM gettext_i18n_rails (>= 0.7.1) po_to_json (>= 1.0.0) rails (>= 3.2.0) - gitaly-proto (0.112.0) + gitaly-proto (0.113.0) google-protobuf (~> 3.1) grpc (~> 1.10) github-linguist (5.3.3) @@ -1052,12 +1050,11 @@ DEPENDENCIES font-awesome-rails (~> 4.7) foreman (~> 0.84.0) fuubar (~> 2.2.0) - gemnasium-gitlab-service (~> 0.2) gemojione (~> 3.3) gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.3) - gitaly-proto (~> 0.112.0) + gitaly-proto (~> 0.113.0) github-linguist (~> 5.3.3) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-gollum-lib (~> 4.2) @@ -1216,4 +1213,4 @@ DEPENDENCIES wikicloth (= 0.8.1) BUNDLED WITH - 1.16.2 + 1.16.3 diff --git a/README.md b/README.md index b6e1cc9a432..335736e53f5 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ You can access a new installation with the login **`root`** and password **`5ive ## Contributing -GitLab is an open source project and we are very happy to accept community contributions. Please refer to [CONTRIBUTING.md](/CONTRIBUTING.md) for details. +GitLab is an open source project and we are very happy to accept community contributions. Please refer to [Contributing to GitLab page](https://about.gitlab.com/contributing/) for more details. ## Licensing @@ -66,7 +66,7 @@ GitLab Community Edition (CE) is available freely under the MIT Expat license. All third party components incorporated into the GitLab Software are licensed under the original license provided by the owner of the applicable component. -All Documentation content that resides under the doc/ directory of this repository is licensed under Creative Commons: CC BY-SA 4.0. +All Documentation content that resides under the `doc/` directory of this repository is licensed under Creative Commons: CC BY-SA 4.0. ## Install a development environment diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 6afaefc56f8..ae96ac3b80c 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -172,7 +172,7 @@ export default { <template> <div v-if="!showEmptyState" - class="prometheus-graphs prepend-top-10" + class="prometheus-graphs prepend-top-default" > <div class="environments d-flex align-items-center"> {{ s__('Metrics|Environment') }} diff --git a/app/assets/stylesheets/framework/images.scss b/app/assets/stylesheets/framework/images.scss index f878ec1ca91..1e93bf2b751 100644 --- a/app/assets/stylesheets/framework/images.scss +++ b/app/assets/stylesheets/framework/images.scss @@ -25,7 +25,7 @@ &.svg-#{$width} { img, svg { - width: #{$width + 'px'}; + max-width: #{$width + 'px'}; } } } diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb index 86bade49ec9..9e30b982b06 100644 --- a/app/controllers/autocomplete_controller.rb +++ b/app/controllers/autocomplete_controller.rb @@ -1,67 +1,39 @@ class AutocompleteController < ApplicationController - AWARD_EMOJI_MAX = 100 - skip_before_action :authenticate_user!, only: [:users, :award_emojis] - before_action :load_project, only: [:users] - before_action :load_group, only: [:users] def users - @users = AutocompleteUsersFinder.new(params: params, current_user: current_user, project: @project, group: @group).execute - - render json: UserSerializer.new.represent(@users) - end - - def user - @user = User.find(params[:id]) - render json: UserSerializer.new.represent(@user) - end - - def projects - project = Project.find_by_id(params[:project_id]) - projects = projects_finder.execute(project, search: params[:search], offset_id: params[:offset_id]) + project = Autocomplete::ProjectFinder + .new(current_user, params) + .execute - render json: projects.to_json(only: [:id, :name_with_namespace], methods: :name_with_namespace) - end + group = Autocomplete::GroupFinder + .new(current_user, project, params) + .execute - def award_emojis - emoji_with_count = AwardEmoji - .limit(AWARD_EMOJI_MAX) - .where(user: current_user) - .group(:name) - .order('count_all DESC, name ASC') - .count + users = Autocomplete::UsersFinder + .new(params: params, current_user: current_user, project: project, group: group) + .execute - # Transform from hash to array to guarantee json order - # e.g. { 'thumbsup' => 2, 'thumbsdown' = 1 } - # => [{ name: 'thumbsup' }, { name: 'thumbsdown' }] - render json: emoji_with_count.map { |k, v| { name: k } } + render json: UserSerializer.new.represent(users) end - private - - def load_group - @group ||= begin - if @project.blank? && params[:group_id].present? - group = Group.find(params[:group_id]) - return render_404 unless can?(current_user, :read_group, group) + def user + user = UserFinder.new(params).execute! - group - end - end + render json: UserSerializer.new.represent(user) end - def load_project - @project ||= begin - if params[:project_id].present? - project = Project.find(params[:project_id]) - return render_404 unless can?(current_user, :read_project, project) + # Displays projects to use for the dropdown when moving a resource from one + # project to another. + def projects + projects = Autocomplete::MoveToProjectFinder + .new(current_user, params) + .execute - project - end - end + render json: MoveToProjectSerializer.new.represent(projects) end - def projects_finder - MoveToProjectFinder.new(current_user) + def award_emojis + render json: AwardedEmojiFinder.new(current_user).execute end end diff --git a/app/finders/autocomplete/group_finder.rb b/app/finders/autocomplete/group_finder.rb new file mode 100644 index 00000000000..dd97ac4c817 --- /dev/null +++ b/app/finders/autocomplete/group_finder.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Autocomplete + # Finder for retrieving a group to use for autocomplete data sources. + class GroupFinder + attr_reader :current_user, :project, :group_id + + # current_user - The currently logged in user, if any. + # project - The Project (if any) to use for the autocomplete data sources. + # params - A Hash containing parameters to use for finding the project. + # + # The following parameters are supported: + # + # * group_id: The ID of the group to find. + def initialize(current_user = nil, project = nil, params = {}) + @current_user = current_user + @project = project + @group_id = params[:group_id] + end + + # Attempts to find a Group based on the current group ID. + def execute + return unless project.blank? && group_id.present? + + group = Group.find(group_id) + + # This removes the need for using `return render_404` and similar patterns + # in controllers that use this finder. + unless Ability.allowed?(current_user, :read_group, group) + raise ActiveRecord::RecordNotFound + .new("Could not find a Group with ID #{group_id}") + end + + group + end + end +end diff --git a/app/finders/autocomplete/move_to_project_finder.rb b/app/finders/autocomplete/move_to_project_finder.rb new file mode 100644 index 00000000000..edaf74c5f92 --- /dev/null +++ b/app/finders/autocomplete/move_to_project_finder.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Autocomplete + # Finder that retrieves a list of projects that an issue can be moved to. + class MoveToProjectFinder + attr_reader :current_user, :search, :project_id, :offset_id + + # current_user - The User object of the user that wants to view the list of + # projects. + # + # params - A Hash containing additional parameters to set. + # + # The following parameters can be set (as Symbols): + # + # * search: An optional search query to apply to the list of projects. + # * project_id: The ID of a project to exclude from the returned relation. + # * offset_id: The ID of a project to use for pagination. When given, only + # projects with a lower ID are included in the list. + def initialize(current_user, params = {}) + @current_user = current_user + @search = params[:search] + @project_id = params[:project_id] + @offset_id = params[:offset_id] + end + + def execute + current_user + .projects_where_can_admin_issues + .optionally_search(search) + .excluding_project(project_id) + .paginate_in_descending_order_using_id(before: offset_id) + .eager_load_namespace_and_owner + end + end +end diff --git a/app/finders/autocomplete/project_finder.rb b/app/finders/autocomplete/project_finder.rb new file mode 100644 index 00000000000..3a4696f4c2e --- /dev/null +++ b/app/finders/autocomplete/project_finder.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Autocomplete + # Finder for retrieving a project to use for autocomplete data sources. + class ProjectFinder + attr_reader :current_user, :project_id + + # current_user - The currently logged in user, if any. + # params - A Hash containing parameters to use for finding the project. + # + # The following parameters are supported: + # + # * project_id: The ID of the project to find. + def initialize(current_user = nil, params = {}) + @current_user = current_user + @project_id = params[:project_id] + end + + # Attempts to find a Project based on the current project ID. + def execute + return if project_id.blank? + + project = Project.find(project_id) + + # This removes the need for using `return render_404` and similar patterns + # in controllers that use this finder. + unless Ability.allowed?(current_user, :read_project, project) + raise ActiveRecord::RecordNotFound + .new("Could not find a Project with ID #{project_id}") + end + + project + end + end +end diff --git a/app/finders/autocomplete/users_finder.rb b/app/finders/autocomplete/users_finder.rb new file mode 100644 index 00000000000..b2557469079 --- /dev/null +++ b/app/finders/autocomplete/users_finder.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +module Autocomplete + class UsersFinder + # The number of users to display in the results is hardcoded to 20, and + # pagination is not supported. This ensures that performance remains + # consistent and removes the need for implementing keyset pagination to + # ensure good performance. + LIMIT = 20 + + attr_reader :current_user, :project, :group, :search, :skip_users, + :author_id, :todo_filter, :todo_state_filter, + :filter_by_current_user + + def initialize(params:, current_user:, project:, group:) + @current_user = current_user + @project = project + @group = group + @search = params[:search] + @skip_users = params[:skip_users] + @author_id = params[:author_id] + @todo_filter = params[:todo_filter] + @todo_state_filter = params[:todo_state_filter] + @filter_by_current_user = params[:current_user] + end + + def execute + items = limited_users + + if search.blank? + # Include current user if available to filter by "Me" + items.unshift(current_user) if prepend_current_user? + + if prepend_author? && (author = User.find_by_id(author_id)) + items.unshift(author) + end + end + + items.uniq + end + + private + + # Returns the users based on the input parameters, as an Array. + # + # This method is separate so it is easier to extend in EE. + def limited_users + # When changing the order of these method calls, make sure that + # reorder_by_name() is called _before_ optionally_search(), otherwise + # reorder_by_name will break the ORDER BY applied in optionally_search(). + find_users + .active + .reorder_by_name + .optionally_search(search) + .where_not_in(skip_users) + .limit_to_todo_authors( + user: current_user, + with_todos: todo_filter, + todo_state: todo_state_filter + ) + .limit(LIMIT) + .to_a + end + + def prepend_current_user? + filter_by_current_user.present? && current_user + end + + def prepend_author? + author_id.present? && current_user + end + + def find_users + if project + project.authorized_users.union_with_user(author_id) + elsif group + group.users_with_parents + elsif current_user + User.all + else + User.none + end + end + end +end diff --git a/app/finders/autocomplete_users_finder.rb b/app/finders/autocomplete_users_finder.rb deleted file mode 100644 index e8a03947f59..00000000000 --- a/app/finders/autocomplete_users_finder.rb +++ /dev/null @@ -1,68 +0,0 @@ -class AutocompleteUsersFinder - # The number of users to display in the results is hardcoded to 20, and - # pagination is not supported. This ensures that performance remains - # consistent and removes the need for implementing keyset pagination to ensure - # good performance. - LIMIT = 20 - - attr_reader :current_user, :project, :group, :search, :skip_users, - :author_id, :params - - def initialize(params:, current_user:, project:, group:) - @current_user = current_user - @project = project - @group = group - @search = params[:search] - @skip_users = params[:skip_users] - @author_id = params[:author_id] - @params = params - end - - def execute - items = find_users - items = items.active - items = items.reorder(:name) - items = items.search(search) if search.present? - items = items.where.not(id: skip_users) if skip_users.present? - items = items.limit(LIMIT) - - if params[:todo_filter].present? && current_user - items = items.todo_authors(current_user.id, params[:todo_state_filter]) - end - - if search.blank? - # Include current user if available to filter by "Me" - if params[:current_user].present? && current_user - items = [current_user, *items].uniq - end - - if author_id.present? && current_user - author = User.find_by_id(author_id) - items = [author, *items].uniq if author - end - end - - items - end - - private - - def find_users - return users_from_project if project - return group.users_with_parents if group - return User.all if current_user - - User.none - end - - def users_from_project - if author_id.present? - union = Gitlab::SQL::Union - .new([project.authorized_users, User.where(id: author_id)]) - - User.from("(#{union.to_sql}) #{User.table_name}") - else - project.authorized_users - end - end -end diff --git a/app/finders/awarded_emoji_finder.rb b/app/finders/awarded_emoji_finder.rb new file mode 100644 index 00000000000..f0cc17f3b26 --- /dev/null +++ b/app/finders/awarded_emoji_finder.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +# Class for retrieving information about emoji awarded _by_ a particular user. +class AwardedEmojiFinder + attr_reader :current_user + + # current_user - The User to generate the data for. + def initialize(current_user = nil) + @current_user = current_user + end + + def execute + return [] unless current_user + + # We want the resulting data set to be an Array containing the emoji names + # in descending order, based on how often they were awarded. + AwardEmoji + .award_counts_for_user(current_user) + .map { |name, _| { name: name } } + end +end diff --git a/app/finders/move_to_project_finder.rb b/app/finders/move_to_project_finder.rb deleted file mode 100644 index 038d5565a1e..00000000000 --- a/app/finders/move_to_project_finder.rb +++ /dev/null @@ -1,21 +0,0 @@ -class MoveToProjectFinder - PAGE_SIZE = 50 - - def initialize(user) - @user = user - end - - def execute(from_project, search: nil, offset_id: nil) - projects = @user.projects_where_can_admin_issues - projects = projects.search(search) if search.present? - projects = projects.excluding_project(from_project) - projects = projects.order_id_desc - - # infinite scroll using offset - projects = projects.where('projects.id < ?', offset_id) if offset_id.present? - projects = projects.limit(PAGE_SIZE) - - # to ask for Project#name_with_namespace - projects.includes(namespace: :owner) - end -end diff --git a/app/finders/user_finder.rb b/app/finders/user_finder.rb new file mode 100644 index 00000000000..484a93c9873 --- /dev/null +++ b/app/finders/user_finder.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +# A simple finding for obtaining a single User. +# +# While using `User.find_by` directly is straightforward, it can lead to a lot +# of code duplication. Sometimes we just want to find a user by an ID, other +# times we may want to exclude blocked user. By using this finder (and extending +# it whenever necessary) we can keep this logic in one place. +class UserFinder + attr_reader :params + + def initialize(params) + @params = params + end + + # Tries to find a User, returning nil if none could be found. + def execute + User.find_by(id: params[:id]) + end + + # Tries to find a User, raising a `ActiveRecord::RecordNotFound` if it could + # not be found. + def execute! + User.find(params[:id]) + end +end diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb index 99c7866d636..ddc516ccb60 100644 --- a/app/models/award_emoji.rb +++ b/app/models/award_emoji.rb @@ -28,6 +28,23 @@ class AwardEmoji < ActiveRecord::Base .where('name IN (?) AND awardable_type = ? AND awardable_id IN (?)', [DOWNVOTE_NAME, UPVOTE_NAME], type, ids) .group('name', 'awardable_id') end + + # Returns the top 100 emoji awarded by the given user. + # + # The returned value is a Hash mapping emoji names to the number of times + # they were awarded: + # + # { 'thumbsup' => 2, 'thumbsdown' => 1 } + # + # user - The User to get the awards for. + # limt - The maximum number of emoji to return. + def award_counts_for_user(user, limit = 100) + limit(limit) + .where(user: user) + .group(:name) + .order('count_all DESC, name ASC') + .count + end end def downvote? diff --git a/app/models/concerns/optionally_search.rb b/app/models/concerns/optionally_search.rb new file mode 100644 index 00000000000..dec97b7dee8 --- /dev/null +++ b/app/models/concerns/optionally_search.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module OptionallySearch + extend ActiveSupport::Concern + + module ClassMethods + def search(*) + raise( + NotImplementedError, + 'Your model must implement the "search" class method' + ) + end + + # Optionally limits a result set to those matching the given search query. + def optionally_search(query = nil) + query.present? ? search(query) : all + end + end +end diff --git a/app/models/project.rb b/app/models/project.rb index 94c1d60f071..8f631d7f0ed 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -28,6 +28,7 @@ class Project < ActiveRecord::Base include WithUploads include BatchDestroyDependentAssociations include FeatureGate + include OptionallySearch extend Gitlab::Cache::RequestCache extend Gitlab::ConfigHelper @@ -140,7 +141,6 @@ class Project < ActiveRecord::Base has_one :flowdock_service has_one :assembla_service has_one :asana_service - has_one :gemnasium_service has_one :mattermost_slash_commands_service has_one :mattermost_service has_one :slack_slash_commands_service @@ -384,6 +384,26 @@ class Project < ActiveRecord::Base only_integer: true, message: 'needs to be beetween 10 minutes and 1 month' } + # Paginates a collection using a `WHERE id < ?` condition. + # + # before - A project ID to use for filtering out projects with an equal or + # greater ID. If no ID is given, all projects are included. + # + # limit - The maximum number of rows to include. + def self.paginate_in_descending_order_using_id( + before: nil, + limit: Kaminari.config.default_per_page + ) + relation = order_id_desc.limit(limit) + relation = relation.where('projects.id < ?', before) if before + + relation + end + + def self.eager_load_namespace_and_owner + includes(namespace: :owner) + end + # Returns a collection of projects that is either public or visible to the # logged in user. def self.public_or_visible_to_user(user = nil) diff --git a/app/models/project_services/gemnasium_service.rb b/app/models/project_services/gemnasium_service.rb deleted file mode 100644 index 67a92c441b1..00000000000 --- a/app/models/project_services/gemnasium_service.rb +++ /dev/null @@ -1,62 +0,0 @@ -# frozen_string_literal: true - -require "gemnasium/gitlab_service" - -class GemnasiumService < Service - prop_accessor :token, :api_key - validates :token, :api_key, presence: true, if: :activated? - validate :deprecation_validation - - def title - 'Gemnasium' - end - - def description - 'Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities.' - end - - def self.to_param - 'gemnasium' - end - - def fields - [ - { type: 'text', name: 'api_key', placeholder: 'Your personal API KEY on gemnasium.com ', required: true }, - { type: 'text', name: 'token', placeholder: 'The project\'s slug on gemnasium.com', required: true } - ] - end - - def self.supported_events - %w(push) - end - - def deprecated? - true - end - - def deprecation_message - "Gemnasium has been acquired by GitLab in January 2018. Since May 15, 2018, the service provided by Gemnasium is no longer available." - end - - def deprecation_validation - errors[:base] << deprecation_message - end - - def execute(data) - return unless supported_events.include?(data[:object_kind]) - - # Gitaly: this class will be removed https://gitlab.com/gitlab-org/gitlab-ee/issues/6010 - repo_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do - project.repository.path_to_repo - end - - Gemnasium::GitlabService.execute( - ref: data[:ref], - before: data[:before], - after: data[:after], - token: token, - api_key: api_key, - repo: repo_path - ) - end -end diff --git a/app/models/user.rb b/app/models/user.rb index 13b04270a4a..a6ba90794d6 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -19,6 +19,7 @@ class User < ActiveRecord::Base include BulkMemberAccessLoad include BlocksJsonSerialization include WithUploads + include OptionallySearch DEFAULT_NOTIFICATION_LEVEL = :participating @@ -253,11 +254,41 @@ class User < ActiveRecord::Base scope :external, -> { where(external: true) } scope :active, -> { with_state(:active).non_internal } scope :without_projects, -> { joins('LEFT JOIN project_authorizations ON users.id = project_authorizations.user_id').where(project_authorizations: { user_id: nil }) } - scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) } scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'DESC')) } scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'ASC')) } scope :confirmed, -> { where.not(confirmed_at: nil) } + # Limits the users to those that have TODOs, optionally in the given state. + # + # user - The user to get the todos for. + # + # with_todos - If we should limit the result set to users that are the + # authors of todos. + # + # todo_state - An optional state to require the todos to be in. + def self.limit_to_todo_authors(user: nil, with_todos: false, todo_state: nil) + if user && with_todos + where(id: Todo.where(user: user, state: todo_state).select(:author_id)) + else + all + end + end + + # Returns a relation that optionally includes the given user. + # + # user_id - The ID of the user to include. + def self.union_with_user(user_id = nil) + if user_id.present? + union = Gitlab::SQL::Union.new([all, User.unscoped.where(id: user_id)]) + + # We use "unscoped" here so that any inner conditions are not repeated for + # the outer query, which would be redundant. + User.unscoped.from("(#{union.to_sql}) #{User.table_name}") + else + all + end + end + def self.with_two_factor_indistinct joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id") .where("u2f.id IS NOT NULL OR users.otp_required_for_login = ?", true) @@ -365,6 +396,18 @@ class User < ActiveRecord::Base ).reorder(order % { query: ActiveRecord::Base.connection.quote(query) }, :name) end + # Limits the result set to users _not_ in the given query/list of IDs. + # + # users - The list of users to ignore. This can be an + # `ActiveRecord::Relation`, or an Array. + def where_not_in(users = nil) + users ? where.not(id: users) : all + end + + def reorder_by_name + reorder(:name) + end + # searches user by given pattern # it compares name, email, username fields and user's secondary emails with given pattern # This method uses ILIKE on PostgreSQL and LIKE on MySQL. diff --git a/app/serializers/move_to_project_entity.rb b/app/serializers/move_to_project_entity.rb new file mode 100644 index 00000000000..dac1124b0b3 --- /dev/null +++ b/app/serializers/move_to_project_entity.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class MoveToProjectEntity < Grape::Entity + expose :id + expose :name_with_namespace +end diff --git a/app/serializers/move_to_project_serializer.rb b/app/serializers/move_to_project_serializer.rb new file mode 100644 index 00000000000..6a59317505c --- /dev/null +++ b/app/serializers/move_to_project_serializer.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class MoveToProjectSerializer < BaseSerializer + entity MoveToProjectEntity +end diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml index 8dfd176f1b7..9280ff4d478 100644 --- a/app/views/admin/runners/index.html.haml +++ b/app/views/admin/runners/index.html.haml @@ -49,7 +49,7 @@ = submit_tag 'Search', class: 'btn' .float-right.light - Runners with last contact more than a minute ago: #{@active_runners_cnt} + Runners currently online: #{@active_runners_cnt} %br diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml index af86b8e8e67..4222963a754 100644 --- a/app/views/projects/environments/metrics.html.haml +++ b/app/views/projects/environments/metrics.html.haml @@ -2,11 +2,4 @@ - page_title "Metrics for environment", @environment.name .prometheus-container{ class: container_class } - .top-area - .row - .col-sm-6 - %h3 - Environment: - = link_to @environment.name, environment_path(@environment) - #prometheus-graphs{ data: metrics_data(@project, @environment) } diff --git a/app/views/projects/runners/_specific_runners.html.haml b/app/views/projects/runners/_specific_runners.html.haml index 6c11ce3b394..314af44490e 100644 --- a/app/views/projects/runners/_specific_runners.html.haml +++ b/app/views/projects/runners/_specific_runners.html.haml @@ -13,4 +13,4 @@ %h4.underlined-title Available specific runners %ul.bordered-list.available-specific-runners = render partial: 'projects/runners/runner', collection: @assignable_runners, as: :runner - = paginate @assignable_runners, theme: "gitlab" + = paginate @assignable_runners, theme: "gitlab", :params => { :anchor => '#js-runners-settings' } diff --git a/changelogs/unreleased/42754-runners-pagination.yml b/changelogs/unreleased/42754-runners-pagination.yml new file mode 100644 index 00000000000..8e77b857538 --- /dev/null +++ b/changelogs/unreleased/42754-runners-pagination.yml @@ -0,0 +1,5 @@ +--- +title: Does not collapse runners section when using pagination +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/47845-propagate_failure_reason-to-job-webhook.yml b/changelogs/unreleased/47845-propagate_failure_reason-to-job-webhook.yml new file mode 100644 index 00000000000..3f85f75bef4 --- /dev/null +++ b/changelogs/unreleased/47845-propagate_failure_reason-to-job-webhook.yml @@ -0,0 +1,5 @@ +--- +title: "#47845 Add failure_reason to job webhook" +merge_request: 21143 +author: matemaciek +type: added diff --git a/changelogs/unreleased/48145-illustration.yml b/changelogs/unreleased/48145-illustration.yml new file mode 100644 index 00000000000..7d84075c2b3 --- /dev/null +++ b/changelogs/unreleased/48145-illustration.yml @@ -0,0 +1,5 @@ +--- +title: Fixes SVGs for empty states in job page overflowing on mobile +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/48869-wiki-slugs-with-spaces.yml b/changelogs/unreleased/48869-wiki-slugs-with-spaces.yml new file mode 100644 index 00000000000..88ba8028e2c --- /dev/null +++ b/changelogs/unreleased/48869-wiki-slugs-with-spaces.yml @@ -0,0 +1,5 @@ +--- +title: Allow spaces in wiki markdown links when using CommonMark +merge_request: 20417 +author: +type: fixed diff --git a/changelogs/unreleased/49907-commits-and-merge-requests-does-not-list-all-files-when-one-file-exceeds-size-limits.yml b/changelogs/unreleased/49907-commits-and-merge-requests-does-not-list-all-files-when-one-file-exceeds-size-limits.yml new file mode 100644 index 00000000000..2fce00a662f --- /dev/null +++ b/changelogs/unreleased/49907-commits-and-merge-requests-does-not-list-all-files-when-one-file-exceeds-size-limits.yml @@ -0,0 +1,5 @@ +--- +title: Fix merge requests not showing any diff files for big patches +merge_request: 21125 +author: +type: fixed diff --git a/changelogs/unreleased/50019-remove-redundant-header-from-metrics-page.yml b/changelogs/unreleased/50019-remove-redundant-header-from-metrics-page.yml new file mode 100644 index 00000000000..8057819b223 --- /dev/null +++ b/changelogs/unreleased/50019-remove-redundant-header-from-metrics-page.yml @@ -0,0 +1,5 @@ +--- +title: Remove redundant header from metrics page +merge_request: 21282 +author: +type: changed diff --git a/changelogs/unreleased/6010_remove_gemnasium_service.yml b/changelogs/unreleased/6010_remove_gemnasium_service.yml new file mode 100644 index 00000000000..900e84c9eed --- /dev/null +++ b/changelogs/unreleased/6010_remove_gemnasium_service.yml @@ -0,0 +1,5 @@ +--- +title: Remove Gemnasium service +merge_request: 21185 +author: +type: removed diff --git a/changelogs/unreleased/fix_event_api_permissions.yml b/changelogs/unreleased/fix_event_api_permissions.yml new file mode 100644 index 00000000000..dee280e93ad --- /dev/null +++ b/changelogs/unreleased/fix_event_api_permissions.yml @@ -0,0 +1,5 @@ +--- +title: 'Events API now requires the read_user or api scope.' +merge_request: 20627 +author: Warren Parad +type: fixed diff --git a/changelogs/unreleased/runners-online.yml b/changelogs/unreleased/runners-online.yml new file mode 100644 index 00000000000..a732d9cb723 --- /dev/null +++ b/changelogs/unreleased/runners-online.yml @@ -0,0 +1,5 @@ +--- +title: Clarify current runners online text +merge_request: 21151 +author: Ben Bodenmiller +type: other diff --git a/doc/administration/high_availability/gitlab.md b/doc/administration/high_availability/gitlab.md index 637d44d2823..b74554a5598 100644 --- a/doc/administration/high_availability/gitlab.md +++ b/doc/administration/high_availability/gitlab.md @@ -25,11 +25,11 @@ for each GitLab application server in your environment. options. Here is an example snippet to add to `/etc/fstab`: ``` - 10.1.0.1:/var/opt/gitlab/.ssh /var/opt/gitlab/.ssh nfs defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2 - 10.1.0.1:/var/opt/gitlab/gitlab-rails/uploads /var/opt/gitlab/gitlab-rails/uploads nfs defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2 - 10.1.0.1:/var/opt/gitlab/gitlab-rails/shared /var/opt/gitlab/gitlab-rails/shared nfs defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2 - 10.1.0.1:/var/opt/gitlab/gitlab-ci/builds /var/opt/gitlab/gitlab-ci/builds nfs defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2 - 10.1.0.1:/var/opt/gitlab/git-data /var/opt/gitlab/git-data nfs defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2 + 10.1.0.1:/var/opt/gitlab/.ssh /var/opt/gitlab/.ssh nfs4 defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2 + 10.1.0.1:/var/opt/gitlab/gitlab-rails/uploads /var/opt/gitlab/gitlab-rails/uploads nfs4 defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2 + 10.1.0.1:/var/opt/gitlab/gitlab-rails/shared /var/opt/gitlab/gitlab-rails/shared nfs4 defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2 + 10.1.0.1:/var/opt/gitlab/gitlab-ci/builds /var/opt/gitlab/gitlab-ci/builds nfs4 defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2 + 10.1.0.1:/var/opt/gitlab/git-data /var/opt/gitlab/git-data nfs4 defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2 ``` 1. Create the shared directories. These may be different depending on your NFS diff --git a/doc/administration/repository_storage_types.md b/doc/administration/repository_storage_types.md index 88221db78f1..bd758c49eba 100644 --- a/doc/administration/repository_storage_types.md +++ b/doc/administration/repository_storage_types.md @@ -42,7 +42,7 @@ Registry, etc. ## Hashed Storage > **Warning:** Hashed storage is in **Beta**. For the latest updates, check the -> associated [issue](https://gitlab.com/gitlab-com/infrastructure/issues/2821) +> associated [issue](https://gitlab.com/gitlab-com/infrastructure/issues/3542) > and please report any problems you encounter. Hashed Storage is the new storage behavior we are rolling out with 10.0. Instead diff --git a/doc/api/events.md b/doc/api/events.md index 1b6c4d437dd..fb5ebb71a86 100644 --- a/doc/api/events.md +++ b/doc/api/events.md @@ -48,9 +48,11 @@ GitLab removes events older than 1 year from the events table for performance re ## List currently authenticated user's events ->**Note:** This endpoint was introduced in GitLab 9.3. +>**Notes:** +> This endpoint was introduced in GitLab 9.3. +> `read_user` access was introduced in GitLab 11.3. -Get a list of events for the authenticated user. +Get a list of events for the authenticated user. Scope `read_user` or `api` is required. ``` GET /events @@ -119,9 +121,11 @@ Example response: ### Get user contribution events ->**Note:** Documentation was formerly located in the [Users API pages][users-api]. +>**Notes:** +> Documentation was formerly located in the [Users API pages][users-api]. +> `read_user` access was introduced in GitLab 11.3. -Get the contribution events for the specified user, sorted from newest to oldest. +Get the contribution events for the specified user, sorted from newest to oldest. Scope `read_user` or `api` is required. ``` GET /users/:id/events diff --git a/doc/api/services.md b/doc/api/services.md index efa173180bb..8c59b232b6d 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -401,48 +401,6 @@ Get Flowdock service settings for a project. GET /projects/:id/services/flowdock ``` -## Gemnasium - -Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities. - -CAUTION: **Warning:** -Gemnasium service integration has been deprecated in GitLab 11.0. Gemnasium has been -[acquired by GitLab](https://about.gitlab.com/press/releases/2018-01-30-gemnasium-acquisition.html) -in January 2018 and since May 15, 2018, the service provided by Gemnasium is no longer available. -You can [migrate from Gemnasium to GitLab](https://docs.gitlab.com/ee/user/project/import/gemnasium.html) -to keep monitoring your dependencies. - -### Create/Edit Gemnasium service - -Set Gemnasium service for a project. - -``` -PUT /projects/:id/services/gemnasium -``` - -Parameters: - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `api_key` | string | true | Your personal API KEY on gemnasium.com | -| `token` | string | true | The project's slug on gemnasium.com | - -### Delete Gemnasium service - -Delete Gemnasium service for a project. - -``` -DELETE /projects/:id/services/gemnasium -``` - -### Get Gemnasium service settings - -Get Gemnasium service settings for a project. - -``` -GET /projects/:id/services/gemnasium -``` - ## Hangouts Chat Google GSuite team collaboration tool. diff --git a/doc/ci/README.md b/doc/ci/README.md index 7666219acb0..d782d64e971 100644 --- a/doc/ci/README.md +++ b/doc/ci/README.md @@ -76,6 +76,8 @@ learn how to leverage its potential even more. - [Trigger pipelines on a schedule](../user/project/pipelines/schedules.md) - [Kubernetes clusters](../user/project/clusters/index.md) - Integrate one or more Kubernetes clusters to your project +- [Interactive web terminal](interactive_web_terminal/index.md) - Open an interactive + web terminal to debug the running jobs ## GitLab CI/CD for Docker diff --git a/doc/ci/interactive_web_terminal/img/finished_job_with_terminal_open.png b/doc/ci/interactive_web_terminal/img/finished_job_with_terminal_open.png Binary files differnew file mode 100644 index 00000000000..199268a1486 --- /dev/null +++ b/doc/ci/interactive_web_terminal/img/finished_job_with_terminal_open.png diff --git a/doc/ci/interactive_web_terminal/img/interactive_web_terminal_page.png b/doc/ci/interactive_web_terminal/img/interactive_web_terminal_page.png Binary files differnew file mode 100644 index 00000000000..b59c1b6bc43 --- /dev/null +++ b/doc/ci/interactive_web_terminal/img/interactive_web_terminal_page.png diff --git a/doc/ci/interactive_web_terminal/img/interactive_web_terminal_running_job.png b/doc/ci/interactive_web_terminal/img/interactive_web_terminal_running_job.png Binary files differnew file mode 100644 index 00000000000..f92c6df07a1 --- /dev/null +++ b/doc/ci/interactive_web_terminal/img/interactive_web_terminal_running_job.png diff --git a/doc/ci/interactive_web_terminal/index.md b/doc/ci/interactive_web_terminal/index.md new file mode 100644 index 00000000000..507aceb27fa --- /dev/null +++ b/doc/ci/interactive_web_terminal/index.md @@ -0,0 +1,52 @@ +# Getting started with interactive web terminals + +> Introduced in GitLab 11.3. + +CAUTION: **Warning:** +Interactive web terminals are in beta, so they might not work properly and +lack features. For more information [follow issue #25990](https://gitlab.com/gitlab-org/gitlab-ce/issues/25990). + +Interactive web terminals give the user access to a terminal in GitLab for +running one-of commands for their CI pipeline. + +NOTE: **Note:** +This is not available for the shared Runners on GitLab.com. +To make use of this feature, you need to provide your +[own Runner](https://docs.gitlab.com/runner/install/) and properly +[configure it](#configuration). + +## Configuration + +Two things need to be configured for the interactive web terminal to work: + +- The Runner needs to have [`[session_server]` configured + properly][session-server] +- Web terminals need to be + [enabled](../../administration/integration/terminal.md#enabling-and-disabling-terminal-support) + +## Debugging a running job + +NOTE: **Note:** Not all executors are +[supported](https://docs.gitlab.com/runner/executors/#compatibility-chart). + +Sometimes, when a job is running, things don't go as you would expect, and it +would be helpful if one can have a shell to aid debugging. When a job is +running, on the right panel you can see a button `debug` that will open the terminal +for the current job. + +![Example of job running with terminal +available](img/interactive_web_terminal_running_job.png) + +When clicked, a new tab will open to the terminal page where you can access +the terminal and type commands like a normal shell. + +![terminal of the job](img/interactive_web_terminal_page.png) + +If you have the terminal open and the job has finished with its tasks, the +terminal will block the job from finishing for the duration configured in +[`[session_server].terminal_max_retention_time`][session-server] until you +close the terminal window. + +![finished job with terminal open](img/finished_job_with_terminal_open.png) + +[session-server]: https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-session_server-section diff --git a/doc/development/testing_guide/smoke.md b/doc/development/testing_guide/smoke.md new file mode 100644 index 00000000000..3f2843cba6e --- /dev/null +++ b/doc/development/testing_guide/smoke.md @@ -0,0 +1,16 @@ +# Smoke Tests + +It is imperative in any testing suite that we have Smoke Tests. In short, smoke tests are will run quick sanity +end-to-end functional tests from GitLab QA and are designed to run against the specified environment to ensure that +basic functionality is working. + +Currently, our suite consists of this basic functionality coverage: + +- User Login (Standard Auth) +- Project Creation +- Issue Creation +- Merge Request Creation + +--- + +[Return to Testing documentation](index.md) diff --git a/doc/development/testing_guide/testing_levels.md b/doc/development/testing_guide/testing_levels.md index 07ced36f0c1..4403072e96f 100644 --- a/doc/development/testing_guide/testing_levels.md +++ b/doc/development/testing_guide/testing_levels.md @@ -120,6 +120,14 @@ running feature tests (i.e. using Capybara) against it. The actual test scenarios and steps are [part of GitLab Rails] so that they're always in-sync with the codebase. +### Smoke tests + +Smoke tests are quick tests that may be run at any time (especially after the pre-deployment migrations). + +Much like feature tests - these tests run against the UI and ensure that basic functionality is working. + +> See [Smoke Tests](smoke.md) for more information. + Read a separate document about [end-to-end tests](end_to_end_tests.md) to learn more. diff --git a/doc/install/kubernetes/gitlab_omnibus.md b/doc/install/kubernetes/gitlab_omnibus.md index 9aee6b9dc74..c2c8a7a92fd 100644 --- a/doc/install/kubernetes/gitlab_omnibus.md +++ b/doc/install/kubernetes/gitlab_omnibus.md @@ -120,7 +120,7 @@ gitlabConfigStorageSize: 1Gi Ingress routing and SSL are automatically configured within this Chart. An NGINX ingress is provisioned and configured, and will route traffic to any service. SSL certificates are automatically created and configured by [kube-lego](https://github.com/kubernetes/charts/tree/master/stable/kube-lego). > **Note:** -Let's Encrypt limits a single TLD to five certificate requests within a single week. This means that common DNS wildcard services like [nip.io](http://nip.io) and [nip.io](http://nip.io) are unlikely to work. +Let's Encrypt limits a single TLD to five certificate requests within a single week. This means that common DNS wildcard services like [nip.io](http://nip.io) are unlikely to work. ## Installing GitLab using the Helm Chart > **Note:** diff --git a/doc/integration/README.md b/doc/integration/README.md index 54e78bdef54..8a93d4cb84b 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -30,7 +30,7 @@ Bitbucket.org account ## Project services -Integration with services such as Campfire, Flowdock, Gemnasium, HipChat, +Integration with services such as Campfire, Flowdock, HipChat, Pivotal Tracker, and Slack are available in the form of a [Project Service][]. [Project Service]: ../user/project/integrations/project_services.md diff --git a/doc/user/project/integrations/project_services.md b/doc/user/project/integrations/project_services.md index 05ee1b4e6d7..efb0381d7aa 100644 --- a/doc/user/project/integrations/project_services.md +++ b/doc/user/project/integrations/project_services.md @@ -34,7 +34,6 @@ Click on the service links to see further configuration instructions and details | [Emails on push](emails_on_push.md) | Email the commits and diff of each push to a list of recipients | | External Wiki | Replaces the link to the internal wiki with a link to an external wiki | | Flowdock | Flowdock is a collaboration web app for technical teams | -| Gemnasium _(Has been deprecated in GitLab 11.0)_ | Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities | | [Hangouts Chat](hangouts_chat.md) | Receive events notifications in Google Hangouts Chat | | [HipChat](hipchat.md) | Private group chat and IM | | [Irker (IRC gateway)](irker.md) | Send IRC messages, on update, to a list of recipients through an Irker gateway | diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md index 77fa517b5b1..770b1810da1 100644 --- a/doc/user/project/integrations/webhooks.md +++ b/doc/user/project/integrations/webhooks.md @@ -1102,6 +1102,7 @@ X-Gitlab-Event: Build Hook "build_finished_at": null, "build_duration": null, "build_allow_failure": false, + "build_failure_reason": "script_failure", "project_id": 380, "project_name": "gitlab-org/gitlab-test", "user": { diff --git a/lib/api/events.rb b/lib/api/events.rb index fc4ba5a3188..a415508a632 100644 --- a/lib/api/events.rb +++ b/lib/api/events.rb @@ -1,6 +1,7 @@ module API class Events < Grape::API include PaginationParams + include APIGuard helpers do params :event_filter_params do @@ -24,6 +25,8 @@ module API end resource :events do + allow_access_with_scope :read_user, if: -> (request) { request.get? } + desc "List currently authenticated user's events" do detail 'This feature was introduced in GitLab 9.3.' success Entities::Event @@ -46,6 +49,8 @@ module API requires :id, type: String, desc: 'The ID or Username of the user' end resource :users do + allow_access_with_scope :read_user, if: -> (request) { request.get? } + desc 'Get the contribution events of a specified user' do detail 'This feature was introduced in GitLab 8.13.' success Entities::Event diff --git a/lib/api/services.rb b/lib/api/services.rb index 1f2bf546cd7..d1a5ee7db35 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -354,20 +354,6 @@ module API desc: 'Flowdock token' } ], - 'gemnasium' => [ - { - required: true, - name: :api_key, - type: String, - desc: 'Your personal API key on gemnasium.com' - }, - { - required: true, - name: :token, - type: String, - desc: "The project's slug on gemnasium.com" - } - ], 'hangouts-chat' => [ { required: true, @@ -695,7 +681,6 @@ module API EmailsOnPushService, ExternalWikiService, FlowdockService, - GemnasiumService, HangoutsChatService, HipchatService, IrkerService, diff --git a/lib/banzai/filter/spaced_link_filter.rb b/lib/banzai/filter/spaced_link_filter.rb new file mode 100644 index 00000000000..574a8a6c7a5 --- /dev/null +++ b/lib/banzai/filter/spaced_link_filter.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require 'uri' + +module Banzai + module Filter + # HTML Filter for markdown links with spaces in the URLs + # + # Based on Banzai::Filter::AutolinkFilter + # + # CommonMark does not allow spaces in the url portion of a link. + # For example, `[example](page slug)` is not valid. However, + # in our wikis, we support (via RedCarpet) this type of link, allowing + # wiki pages to be easily linked by their title. This filter adds that functionality. + # The intent is for this to only be used in Wikis - in general, we want + # to adhere to CommonMark's spec. + # + class SpacedLinkFilter < HTML::Pipeline::Filter + include ActionView::Helpers::TagHelper + + # Pattern to match a standard markdown link + # + # Rubular: http://rubular.com/r/z9EAHxYmKI + LINK_PATTERN = /\[([^\]]+)\]\(([^)"]+)(?: \"([^\"]+)\")?\)/ + + # Text matching LINK_PATTERN inside these elements will not be linked + IGNORE_PARENTS = %w(a code kbd pre script style).to_set + + # The XPath query to use for finding text nodes to parse. + TEXT_QUERY = %Q(descendant-or-self::text()[ + not(#{IGNORE_PARENTS.map { |p| "ancestor::#{p}" }.join(' or ')}) + and contains(., ']\(') + ]).freeze + + def call + return doc if context[:markdown_engine] == :redcarpet + + doc.xpath(TEXT_QUERY).each do |node| + content = node.to_html + + next unless content.match(LINK_PATTERN) + + html = spaced_link_filter(content) + + next if html == content + + node.replace(html) + end + + doc + end + + private + + def spaced_link_match(link) + match = LINK_PATTERN.match(link) + return link unless match && match[1] && match[2] + + # escape the spaces in the url so that it's a valid markdown link, + # then run it through the markdown processor again, let it do its magic + text = match[1] + new_link = match[2].gsub(' ', '%20') + title = match[3] ? " \"#{match[3]}\"" : '' + html = Banzai::Filter::MarkdownFilter.call("[#{text}](#{new_link}#{title})", context) + + # link is wrapped in a <p>, so strip that off + html.sub('<p>', '').chomp('</p>') + end + + def spaced_link_filter(text) + Gitlab::StringRegexMarker.new(CGI.unescapeHTML(text), text.html_safe).mark(LINK_PATTERN) do |link, left:, right:| + spaced_link_match(link) + end + end + end + end +end diff --git a/lib/banzai/pipeline/wiki_pipeline.rb b/lib/banzai/pipeline/wiki_pipeline.rb index c37b8e71cb0..737ff0cc818 100644 --- a/lib/banzai/pipeline/wiki_pipeline.rb +++ b/lib/banzai/pipeline/wiki_pipeline.rb @@ -5,6 +5,7 @@ module Banzai @filters ||= begin super.insert_after(Filter::TableOfContentsFilter, Filter::GollumTagsFilter) .insert_before(Filter::TaskListFilter, Filter::WikiLinkFilter) + .insert_before(Filter::WikiLinkFilter, Filter::SpacedLinkFilter) end end end diff --git a/lib/gitlab/data_builder/build.rb b/lib/gitlab/data_builder/build.rb index 2f1445a050a..0b71b31a476 100644 --- a/lib/gitlab/data_builder/build.rb +++ b/lib/gitlab/data_builder/build.rb @@ -28,6 +28,7 @@ module Gitlab build_finished_at: build.finished_at, build_duration: build.duration, build_allow_failure: build.allow_failure, + build_failure_reason: build.failure_reason, # TODO: do we still need it? project_id: project.id, diff --git a/lib/gitlab/git/diff.rb b/lib/gitlab/git/diff.rb index b58296375ef..61ce10ca131 100644 --- a/lib/gitlab/git/diff.rb +++ b/lib/gitlab/git/diff.rb @@ -226,6 +226,7 @@ module Gitlab @new_file = diff.from_id == BLANK_SHA @renamed_file = diff.from_path != diff.to_path @deleted_file = diff.to_id == BLANK_SHA + @too_large = diff.too_large if diff.respond_to?(:too_large) collapse! if diff.respond_to?(:collapsed) && diff.collapsed end diff --git a/lib/gitlab/gitaly_client/diff.rb b/lib/gitlab/gitaly_client/diff.rb index d98a0ce988f..af9d674535b 100644 --- a/lib/gitlab/gitaly_client/diff.rb +++ b/lib/gitlab/gitaly_client/diff.rb @@ -1,7 +1,7 @@ module Gitlab module GitalyClient class Diff - ATTRS = %i(from_path to_path old_mode new_mode from_id to_id patch overflow_marker collapsed).freeze + ATTRS = %i(from_path to_path old_mode new_mode from_id to_id patch overflow_marker collapsed too_large).freeze include AttributesBag end diff --git a/lib/gitlab/github_import/importer/diff_note_importer.rb b/lib/gitlab/github_import/importer/diff_note_importer.rb index 8274f37d358..d562958e955 100644 --- a/lib/gitlab/github_import/importer/diff_note_importer.rb +++ b/lib/gitlab/github_import/importer/diff_note_importer.rb @@ -13,7 +13,7 @@ module Gitlab @note = note @project = project @client = client - @user_finder = UserFinder.new(project, client) + @user_finder = GithubImport::UserFinder.new(project, client) end def execute diff --git a/lib/gitlab/github_import/importer/issue_importer.rb b/lib/gitlab/github_import/importer/issue_importer.rb index ead4215810f..cb4d7a6a0b6 100644 --- a/lib/gitlab/github_import/importer/issue_importer.rb +++ b/lib/gitlab/github_import/importer/issue_importer.rb @@ -19,7 +19,7 @@ module Gitlab @issue = issue @project = project @client = client - @user_finder = UserFinder.new(project, client) + @user_finder = GithubImport::UserFinder.new(project, client) @milestone_finder = MilestoneFinder.new(project) @issuable_finder = GithubImport::IssuableFinder.new(project, issue) end diff --git a/lib/gitlab/github_import/importer/note_importer.rb b/lib/gitlab/github_import/importer/note_importer.rb index c890f2df360..2b06d1b3baf 100644 --- a/lib/gitlab/github_import/importer/note_importer.rb +++ b/lib/gitlab/github_import/importer/note_importer.rb @@ -13,7 +13,7 @@ module Gitlab @note = note @project = project @client = client - @user_finder = UserFinder.new(project, client) + @user_finder = GithubImport::UserFinder.new(project, client) end def execute diff --git a/lib/gitlab/github_import/importer/pull_request_importer.rb b/lib/gitlab/github_import/importer/pull_request_importer.rb index e4b49d2143a..ed17aa54373 100644 --- a/lib/gitlab/github_import/importer/pull_request_importer.rb +++ b/lib/gitlab/github_import/importer/pull_request_importer.rb @@ -15,7 +15,7 @@ module Gitlab @pull_request = pull_request @project = project @client = client - @user_finder = UserFinder.new(project, client) + @user_finder = GithubImport::UserFinder.new(project, client) @milestone_finder = MilestoneFinder.new(project) @issuable_finder = GithubImport::IssuableFinder.new(project, pull_request) diff --git a/lib/gitlab/hook_data/issuable_builder.rb b/lib/gitlab/hook_data/issuable_builder.rb index f2eda398b8f..65389835ad7 100644 --- a/lib/gitlab/hook_data/issuable_builder.rb +++ b/lib/gitlab/hook_data/issuable_builder.rb @@ -28,7 +28,7 @@ module Gitlab end def safe_keys - issuable_builder::SAFE_HOOK_ATTRIBUTES + issuable_builder::SAFE_HOOK_RELATIONS + issuable_builder.safe_hook_attributes + issuable_builder::SAFE_HOOK_RELATIONS end private diff --git a/lib/gitlab/hook_data/issue_builder.rb b/lib/gitlab/hook_data/issue_builder.rb index 0d71c748dc6..dd63db969f6 100644 --- a/lib/gitlab/hook_data/issue_builder.rb +++ b/lib/gitlab/hook_data/issue_builder.rb @@ -1,50 +1,52 @@ module Gitlab module HookData class IssueBuilder < BaseBuilder - SAFE_HOOK_ATTRIBUTES = %i[ - assignee_id - author_id - closed_at - confidential - created_at - description - due_date - id - iid - last_edited_at - last_edited_by_id - milestone_id - moved_to_id - project_id - relative_position - state - time_estimate - title - updated_at - updated_by_id - ].freeze - SAFE_HOOK_RELATIONS = %i[ assignees labels total_time_spent ].freeze + def self.safe_hook_attributes + %i[ + assignee_id + author_id + closed_at + confidential + created_at + description + due_date + id + iid + last_edited_at + last_edited_by_id + milestone_id + moved_to_id + project_id + relative_position + state + time_estimate + title + updated_at + updated_by_id + ].freeze + end + alias_method :issue, :object def build attrs = { - description: absolute_image_urls(issue.description), - url: Gitlab::UrlBuilder.build(issue), - total_time_spent: issue.total_time_spent, - human_total_time_spent: issue.human_total_time_spent, - human_time_estimate: issue.human_time_estimate, - assignee_ids: issue.assignee_ids, - assignee_id: issue.assignee_ids.first # This key is deprecated + description: absolute_image_urls(issue.description), + url: Gitlab::UrlBuilder.build(issue), + total_time_spent: issue.total_time_spent, + human_total_time_spent: issue.human_total_time_spent, + human_time_estimate: issue.human_time_estimate, + assignee_ids: issue.assignee_ids, + assignee_id: issue.assignee_ids.first # This key is deprecated } - issue.attributes.with_indifferent_access.slice(*SAFE_HOOK_ATTRIBUTES) - .merge!(attrs) + issue.attributes.with_indifferent_access.slice(*self.class.safe_hook_attributes) + .merge!(attrs) end end end diff --git a/lib/gitlab/hook_data/merge_request_builder.rb b/lib/gitlab/hook_data/merge_request_builder.rb index dfbed0597ed..3aa6a4f3767 100644 --- a/lib/gitlab/hook_data/merge_request_builder.rb +++ b/lib/gitlab/hook_data/merge_request_builder.rb @@ -1,33 +1,35 @@ module Gitlab module HookData class MergeRequestBuilder < BaseBuilder - SAFE_HOOK_ATTRIBUTES = %i[ - assignee_id - author_id - created_at - description - head_pipeline_id - id - iid - last_edited_at - last_edited_by_id - merge_commit_sha - merge_error - merge_params - merge_status - merge_user_id - merge_when_pipeline_succeeds - milestone_id - source_branch - source_project_id - state - target_branch - target_project_id - time_estimate - title - updated_at - updated_by_id - ].freeze + def self.safe_hook_attributes + %i[ + assignee_id + author_id + created_at + description + head_pipeline_id + id + iid + last_edited_at + last_edited_by_id + merge_commit_sha + merge_error + merge_params + merge_status + merge_user_id + merge_when_pipeline_succeeds + milestone_id + source_branch + source_project_id + state + target_branch + target_project_id + time_estimate + title + updated_at + updated_by_id + ].freeze + end SAFE_HOOK_RELATIONS = %i[ assignee @@ -50,8 +52,8 @@ module Gitlab human_time_estimate: merge_request.human_time_estimate } - merge_request.attributes.with_indifferent_access.slice(*SAFE_HOOK_ATTRIBUTES) - .merge!(attrs) + merge_request.attributes.with_indifferent_access.slice(*self.class.safe_hook_attributes) + .merge!(attrs) end end end diff --git a/qa/README.md b/qa/README.md index be4cf89ebbc..f8a5c00efd4 100644 --- a/qa/README.md +++ b/qa/README.md @@ -35,7 +35,7 @@ following call would login to a local [GDK] instance and run all specs in `qa/specs/features`: ``` -bin/qa Test::Instance http://localhost:3000 +bin/qa Test::Instance::All http://localhost:3000 ``` ### Writing tests @@ -48,14 +48,14 @@ You can also supply specific tests to run as another parameter. For example, to run the repository-related specs, you can execute: ``` -bin/qa Test::Instance http://localhost qa/specs/features/repository/ +bin/qa Test::Instance::All http://localhost qa/specs/features/repository/ ``` Since the arguments would be passed to `rspec`, you could use all `rspec` options there. For example, passing `--backtrace` and also line number: ``` -bin/qa Test::Instance http://localhost qa/specs/features/project/create_spec.rb:3 --backtrace +bin/qa Test::Instance::All http://localhost qa/specs/features/project/create_spec.rb:3 --backtrace ``` ### Overriding the authenticated user @@ -67,7 +67,7 @@ If you need to authenticate as a different user, you can provide the `GITLAB_USERNAME` and `GITLAB_PASSWORD` environment variables: ``` -GITLAB_USERNAME=jsmith GITLAB_PASSWORD=password bin/qa Test::Instance https://gitlab.example.com +GITLAB_USERNAME=jsmith GITLAB_PASSWORD=password bin/qa Test::Instance::All https://gitlab.example.com ``` If your user doesn't have permission to default sandbox group @@ -75,13 +75,13 @@ If your user doesn't have permission to default sandbox group `GITLAB_SANDBOX_NAME`: ``` -GITLAB_USERNAME=jsmith GITLAB_PASSWORD=password GITLAB_SANDBOX_NAME=jsmith-qa-sandbox bin/qa Test::Instance https://gitlab.example.com +GITLAB_USERNAME=jsmith GITLAB_PASSWORD=password GITLAB_SANDBOX_NAME=jsmith-qa-sandbox bin/qa Test::Instance::All https://gitlab.example.com ``` In addition, the `GITLAB_USER_TYPE` can be set to "ldap" to sign in as an LDAP user: ``` -GITLAB_USER_TYPE=ldap GITLAB_USERNAME=jsmith GITLAB_PASSWORD=password GITLAB_SANDBOX_NAME=jsmith-qa-sandbox bin/qa Test::Instance https://gitlab.example.com +GITLAB_USER_TYPE=ldap GITLAB_USERNAME=jsmith GITLAB_PASSWORD=password GITLAB_SANDBOX_NAME=jsmith-qa-sandbox bin/qa Test::Instance::All https://gitlab.example.com ``` All [supported environment variables are here](https://gitlab.com/gitlab-org/gitlab-qa#supported-environment-variables). @@ -77,14 +77,16 @@ module QA # autoload :Bootable, 'qa/scenario/bootable' autoload :Actable, 'qa/scenario/actable' - autoload :Taggable, 'qa/scenario/taggable' autoload :Template, 'qa/scenario/template' ## # Test scenario entrypoints. # module Test - autoload :Instance, 'qa/scenario/test/instance' + module Instance + autoload :All, 'qa/scenario/test/instance/all' + autoload :Smoke, 'qa/scenario/test/instance/smoke' + end module Integration autoload :Github, 'qa/scenario/test/integration/github' diff --git a/qa/qa/factory/resource/fork.rb b/qa/qa/factory/resource/fork.rb index 1d0c76a3d30..01969c31438 100644 --- a/qa/qa/factory/resource/fork.rb +++ b/qa/qa/factory/resource/fork.rb @@ -4,7 +4,12 @@ module QA class Fork < Factory::Base dependency Factory::Repository::ProjectPush, as: :push - dependency Factory::Resource::User, as: :user + dependency Factory::Resource::User, as: :user do |user| + if Runtime::Env.forker? + user.username = Runtime::Env.forker_username + user.password = Runtime::Env.forker_password + end + end product(:user) { |factory| factory.user } diff --git a/qa/qa/factory/resource/user.rb b/qa/qa/factory/resource/user.rb index e08df9e0cd0..eac2a873bd5 100644 --- a/qa/qa/factory/resource/user.rb +++ b/qa/qa/factory/resource/user.rb @@ -4,28 +4,52 @@ module QA module Factory module Resource class User < Factory::Base - attr_accessor :name, :username, :email, :password + attr_reader :unique_id + attr_writer :username, :password, :name, :email def initialize - @name = "name-#{SecureRandom.hex(8)}" - @username = "username-#{SecureRandom.hex(8)}" - @email = "mail#{SecureRandom.hex(8)}@mail.com" - @password = 'password' + @unique_id = SecureRandom.hex(8) end - product(:name) { |factory| factory.name } + def username + @username ||= "qa-user-#{unique_id}" + end - product(:username) { |factory| factory.username } + def password + @password ||= 'password' + end - product(:email) { |factory| factory.email } + def name + @name ||= username + end + + def email + @email ||= "#{username}@example.com" + end + + def credentials_given? + defined?(@username) && defined?(@password) + end + product(:name) { |factory| factory.name } + product(:username) { |factory| factory.username } + product(:email) { |factory| factory.email } product(:password) { |factory| factory.password } def fabricate! - Page::Menu::Main.act { sign_out } - Page::Main::Login.act { switch_to_register_tab } - Page::Main::SignUp.perform do |page| - page.sign_up!(name: name, username: username, email: email, password: password) + Page::Menu::Main.perform { |main| main.sign_out } + + if credentials_given? + Page::Main::Login.perform do |login| + login.sign_in_using_credentials(self) + end + else + Page::Main::Login.perform do |login| + login.switch_to_register_tab + end + Page::Main::SignUp.perform do |signup| + signup.sign_up!(self) + end end end end diff --git a/qa/qa/git/repository.rb b/qa/qa/git/repository.rb index 3df6db05970..bdbb18b5045 100644 --- a/qa/qa/git/repository.rb +++ b/qa/qa/git/repository.rb @@ -28,7 +28,7 @@ module QA end def use_default_credentials - self.username = Runtime::User.name + self.username = Runtime::User.username self.password = Runtime::User.password end diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb index 6cdfbd1c125..afc8b66d878 100644 --- a/qa/qa/page/main/login.rb +++ b/qa/qa/page/main/login.rb @@ -40,17 +40,19 @@ module QA end end - def sign_in_using_credentials + def sign_in_using_credentials(user = nil) # Don't try to log-in if we're already logged-in return if Page::Menu::Main.act { has_personal_area?(wait: 0) } using_wait_time 0 do set_initial_password_if_present + raise NotImplementedError if Runtime::User.ldap_user? && user&.credentials_given? + if Runtime::User.ldap_user? sign_in_using_ldap_credentials else - sign_in_using_gitlab_credentials + sign_in_using_gitlab_credentials(user || Runtime::User) end end @@ -69,21 +71,30 @@ module QA click_on 'Register' end + def switch_to_ldap_tab + click_on 'LDAP' + end + + def switch_to_standard_tab + click_on 'Standard' + end + private def sign_in_using_ldap_credentials - click_link 'LDAP' + switch_to_ldap_tab fill_in :username, with: Runtime::User.ldap_username fill_in :password, with: Runtime::User.ldap_password click_button 'Sign in' end - def sign_in_using_gitlab_credentials - click_link 'Standard' if page.has_content?('LDAP') + def sign_in_using_gitlab_credentials(user) + switch_to_sign_in_tab unless page.has_button?('Sign in') + switch_to_standard_tab if page.has_content?('LDAP') - fill_in :user_login, with: Runtime::User.name - fill_in :user_password, with: Runtime::User.password + fill_in :user_login, with: user.username + fill_in :user_password, with: user.password click_button 'Sign in' end diff --git a/qa/qa/page/main/sign_up.rb b/qa/qa/page/main/sign_up.rb index 9a834e94b81..33ab56236f4 100644 --- a/qa/qa/page/main/sign_up.rb +++ b/qa/qa/page/main/sign_up.rb @@ -11,12 +11,12 @@ module QA element :register_button, 'submit "Register"' end - def sign_up!(name:, username:, email:, password:) - fill_in :new_user_name, with: name - fill_in :new_user_username, with: username - fill_in :new_user_email, with: email - fill_in :new_user_email_confirmation, with: email - fill_in :new_user_password, with: password + def sign_up!(user) + fill_in :new_user_name, with: user.name + fill_in :new_user_username, with: user.username + fill_in :new_user_email, with: user.email + fill_in :new_user_email_confirmation, with: user.email + fill_in :new_user_password, with: user.password click_button 'Register' Page::Menu::Main.act { has_personal_area? } diff --git a/qa/qa/page/project/settings/secret_variables.rb b/qa/qa/page/project/settings/secret_variables.rb index d2f5d5a9060..937ae6797c8 100644 --- a/qa/qa/page/project/settings/secret_variables.rb +++ b/qa/qa/page/project/settings/secret_variables.rb @@ -23,7 +23,13 @@ module QA # After we fill the key, JS would generate another field so # we need to use the same index to find the corresponding one. keys[index].set(key) - all_elements(:ci_variable_input_value)[index].set(value) + node = all_elements(:ci_variable_input_value)[index] + + # Simply run `node.set(value)` is too slow for long text here, + # so we need to run JavaScript directly to set the value. + # The code was inspired from: + # https://github.com/teamcapybara/capybara/blob/679548cea10773d45e32808f4d964377cfe5e892/lib/capybara/selenium/node.rb#L217 + execute_script("arguments[0].value = #{value.to_json}", node) end def save_variables diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb index 5dc194e0aef..841c959045f 100644 --- a/qa/qa/runtime/env.rb +++ b/qa/qa/runtime/env.rb @@ -39,6 +39,18 @@ module QA ENV['GITLAB_PASSWORD'] end + def forker? + forker_username && forker_password + end + + def forker_username + ENV['GITLAB_FORKER_USERNAME'] + end + + def forker_password + ENV['GITLAB_FORKER_PASSWORD'] + end + def ldap_username ENV['GITLAB_LDAP_USERNAME'] end diff --git a/qa/qa/runtime/user.rb b/qa/qa/runtime/user.rb index c80ee6d4d96..b016777c987 100644 --- a/qa/qa/runtime/user.rb +++ b/qa/qa/runtime/user.rb @@ -3,12 +3,12 @@ module QA module User extend self - def default_name + def default_username 'root' end - def name - Runtime::Env.user_username || default_name + def username + Runtime::Env.user_username || default_username end def password diff --git a/qa/qa/scenario/taggable.rb b/qa/qa/scenario/taggable.rb deleted file mode 100644 index b1f24d742e0..00000000000 --- a/qa/qa/scenario/taggable.rb +++ /dev/null @@ -1,17 +0,0 @@ -module QA - module Scenario - module Taggable - # rubocop:disable Gitlab/ModuleWithInstanceVariables - - def tags(*tags) - @tags = tags - end - - def focus - @tags.to_a - end - - # rubocop:enable Gitlab/ModuleWithInstanceVariables - end - end -end diff --git a/qa/qa/scenario/template.rb b/qa/qa/scenario/template.rb index d21a9d52997..66eb86f25c8 100644 --- a/qa/qa/scenario/template.rb +++ b/qa/qa/scenario/template.rb @@ -1,15 +1,36 @@ module QA module Scenario class Template - def self.perform(*args) - new.tap do |scenario| - yield scenario if block_given? - break scenario.perform(*args) + class << self + def perform(*args) + new.tap do |scenario| + yield scenario if block_given? + break scenario.perform(*args) + end + end + + def tags(*tags) + @tags = tags + end + + def focus + @tags.to_a end end - def perform(*_args) - raise NotImplementedError + def perform(address, *rspec_options) + Runtime::Scenario.define(:gitlab_address, address) + + Specs::Runner.perform do |specs| + specs.tty = true + specs.tags = self.class.focus + specs.options = + if rspec_options.any? + rspec_options + else + ::File.expand_path('../specs/features', __dir__) + end + end end end end diff --git a/qa/qa/scenario/test/instance.rb b/qa/qa/scenario/test/instance.rb deleted file mode 100644 index 46eb2dabb11..00000000000 --- a/qa/qa/scenario/test/instance.rb +++ /dev/null @@ -1,36 +0,0 @@ -module QA - module Scenario - module Test - ## - # Base class for running the suite against any GitLab instance, - # including staging and on-premises installation. - # - class Instance < Template - include Bootable - extend Taggable - - tags :core - - def perform(address, *rspec_options) - Runtime::Scenario.define(:gitlab_address, address) - - ## - # Perform before hooks, which are different for CE and EE - # - Runtime::Release.perform_before_hooks - - Specs::Runner.perform do |specs| - specs.tty = true - specs.tags = self.class.focus - specs.options = - if rspec_options.any? - rspec_options - else - ::File.expand_path('../../specs/features', __dir__) - end - end - end - end - end - end -end diff --git a/qa/qa/scenario/test/instance/all.rb b/qa/qa/scenario/test/instance/all.rb new file mode 100644 index 00000000000..a07c26431bd --- /dev/null +++ b/qa/qa/scenario/test/instance/all.rb @@ -0,0 +1,15 @@ +module QA + module Scenario + module Test + ## + # Base class for running the suite against any GitLab instance, + # including staging and on-premises installation. + # + module Instance + class All < Template + include Bootable + end + end + end + end +end diff --git a/qa/qa/scenario/test/instance/smoke.rb b/qa/qa/scenario/test/instance/smoke.rb new file mode 100644 index 00000000000..a7d2cb27f27 --- /dev/null +++ b/qa/qa/scenario/test/instance/smoke.rb @@ -0,0 +1,17 @@ +module QA + module Scenario + module Test + module Instance + ## + # Base class for running the suite against any GitLab instance, + # including staging and on-premises installation. + # + class Smoke < Template + include Bootable + + tags :smoke + end + end + end + end +end diff --git a/qa/qa/specs/features/api/basics_spec.rb b/qa/qa/specs/features/api/basics_spec.rb index 6563b56d1b4..bc0b5ebfe10 100644 --- a/qa/qa/specs/features/api/basics_spec.rb +++ b/qa/qa/specs/features/api/basics_spec.rb @@ -1,13 +1,13 @@ require 'securerandom' module QA - describe 'API basics', :core do + describe 'API basics' do before(:context) do @api_client = Runtime::API::Client.new(:gitlab) end let(:project_name) { "api-basics-#{SecureRandom.hex(8)}" } - let(:sanitized_project_path) { CGI.escape("#{Runtime::User.name}/#{project_name}") } + let(:sanitized_project_path) { CGI.escape("#{Runtime::User.username}/#{project_name}") } it 'user creates a project with a file and deletes them afterwards' do create_project_request = Runtime::API::Request.new(@api_client, '/projects') diff --git a/qa/qa/specs/features/api/users_spec.rb b/qa/qa/specs/features/api/users_spec.rb index 8a63d8095c9..3d25cca1e59 100644 --- a/qa/qa/specs/features/api/users_spec.rb +++ b/qa/qa/specs/features/api/users_spec.rb @@ -1,5 +1,5 @@ module QA - describe 'API users', :core do + describe 'API users' do before(:context) do @api_client = Runtime::API::Client.new(:gitlab) end @@ -14,11 +14,11 @@ module QA end it 'submit request with a valid user name' do - get request.url, { params: { username: Runtime::User.name } } + get request.url, { params: { username: Runtime::User.username } } expect_status(200) expect(json_body).to contain_exactly( - a_hash_including(username: Runtime::User.name) + a_hash_including(username: Runtime::User.username) ) end diff --git a/qa/qa/specs/features/login/basic_spec.rb b/qa/qa/specs/features/login/basic_spec.rb new file mode 100644 index 00000000000..f866466c7bf --- /dev/null +++ b/qa/qa/specs/features/login/basic_spec.rb @@ -0,0 +1,15 @@ +module QA + describe 'basic user login', :smoke do + it 'user logs in using basic credentials' do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.act { sign_in_using_credentials } + + # TODO, since `Signed in successfully` message was removed + # this is the only way to tell if user is signed in correctly. + # + Page::Menu::Main.perform do |menu| + expect(menu).to have_personal_area + end + end + end +end diff --git a/qa/qa/specs/features/login/ldap_spec.rb b/qa/qa/specs/features/login/ldap_spec.rb index b7a284c584b..de6111eea64 100644 --- a/qa/qa/specs/features/login/ldap_spec.rb +++ b/qa/qa/specs/features/login/ldap_spec.rb @@ -1,5 +1,5 @@ module QA - describe 'LDAP user login', :ldap do + describe 'LDAP user login', :orchestrated, :ldap do before do Runtime::Env.user_type = 'ldap' end diff --git a/qa/qa/specs/features/mattermost/group_create_spec.rb b/qa/qa/specs/features/mattermost/group_create_spec.rb index a59761da341..097e1713aef 100644 --- a/qa/qa/specs/features/mattermost/group_create_spec.rb +++ b/qa/qa/specs/features/mattermost/group_create_spec.rb @@ -1,5 +1,5 @@ module QA - describe 'create a new group', :mattermost do + describe 'create a new group', :orchestrated, :mattermost do it 'creating a group with a mattermost team' do Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } diff --git a/qa/qa/specs/features/mattermost/login_spec.rb b/qa/qa/specs/features/mattermost/login_spec.rb index b140191e160..27f7d4c245f 100644 --- a/qa/qa/specs/features/mattermost/login_spec.rb +++ b/qa/qa/specs/features/mattermost/login_spec.rb @@ -1,5 +1,5 @@ module QA - describe 'logging in to Mattermost', :mattermost do + describe 'logging in to Mattermost', :orchestrated, :mattermost do it 'can use gitlab oauth' do Runtime::Browser.visit(:gitlab, Page::Main::Login) do Page::Main::Login.act { sign_in_using_credentials } diff --git a/qa/qa/specs/features/merge_request/create_spec.rb b/qa/qa/specs/features/merge_request/create_spec.rb index 36d7efb02e1..71e79956b85 100644 --- a/qa/qa/specs/features/merge_request/create_spec.rb +++ b/qa/qa/specs/features/merge_request/create_spec.rb @@ -1,5 +1,5 @@ module QA - describe 'creates a merge request', :core do + describe 'creates a merge request with milestone' do it 'user creates a new merge request' do Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } @@ -29,4 +29,25 @@ module QA end end end + + describe 'creates a merge request', :smoke do + it 'user creates a new merge request' do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.act { sign_in_using_credentials } + + current_project = Factory::Resource::Project.fabricate! do |project| + project.name = 'project-with-merge-request' + end + + Factory::Resource::MergeRequest.fabricate! do |merge_request| + merge_request.title = 'This is a merge request' + merge_request.description = 'Great feature' + merge_request.project = current_project + end + + expect(page).to have_content('This is a merge request') + expect(page).to have_content('Great feature') + expect(page).to have_content(/Opened [\w\s]+ ago/) + end + end end diff --git a/qa/qa/specs/features/merge_request/rebase_spec.rb b/qa/qa/specs/features/merge_request/rebase_spec.rb index 163dcbe7963..c36d28e4237 100644 --- a/qa/qa/specs/features/merge_request/rebase_spec.rb +++ b/qa/qa/specs/features/merge_request/rebase_spec.rb @@ -1,5 +1,5 @@ module QA - describe 'merge request rebase', :core do + describe 'merge request rebase' do it 'rebases source branch of merge request' do Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } diff --git a/qa/qa/specs/features/merge_request/squash_spec.rb b/qa/qa/specs/features/merge_request/squash_spec.rb index 4856bbe1a69..3ecc36a5ae1 100644 --- a/qa/qa/specs/features/merge_request/squash_spec.rb +++ b/qa/qa/specs/features/merge_request/squash_spec.rb @@ -1,5 +1,5 @@ module QA - describe 'merge request squash commits', :core do + describe 'merge request squash commits' do it 'when squash commits is marked before merge' do Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } diff --git a/qa/qa/specs/features/project/activity_spec.rb b/qa/qa/specs/features/project/activity_spec.rb index 02074e386b6..c7ce8dfdcc6 100644 --- a/qa/qa/specs/features/project/activity_spec.rb +++ b/qa/qa/specs/features/project/activity_spec.rb @@ -1,5 +1,5 @@ module QA - describe 'activity page', :core do + describe 'activity page' do it 'push creates an event in the activity page' do Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } diff --git a/qa/qa/specs/features/project/add_deploy_key_spec.rb b/qa/qa/specs/features/project/add_deploy_key_spec.rb index 14642af97ad..24f9f4c77f8 100644 --- a/qa/qa/specs/features/project/add_deploy_key_spec.rb +++ b/qa/qa/specs/features/project/add_deploy_key_spec.rb @@ -1,5 +1,5 @@ module QA - describe 'deploy keys support', :core do + describe 'deploy keys support' do it 'user adds a deploy key' do Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } diff --git a/qa/qa/specs/features/project/add_secret_variable_spec.rb b/qa/qa/specs/features/project/add_secret_variable_spec.rb index 32c91dd9d4d..04d9fe488e2 100644 --- a/qa/qa/specs/features/project/add_secret_variable_spec.rb +++ b/qa/qa/specs/features/project/add_secret_variable_spec.rb @@ -1,5 +1,5 @@ module QA - describe 'secret variables support', :core do + describe 'secret variables support' do it 'user adds a secret variable' do Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } diff --git a/qa/qa/specs/features/project/auto_devops_spec.rb b/qa/qa/specs/features/project/auto_devops_spec.rb index c2c3bef98e4..248669b6046 100644 --- a/qa/qa/specs/features/project/auto_devops_spec.rb +++ b/qa/qa/specs/features/project/auto_devops_spec.rb @@ -1,7 +1,7 @@ require 'pathname' module QA - describe 'Auto Devops', :kubernetes do + describe 'Auto Devops', :orchestrated, :kubernetes do after do @cluster&.remove! end @@ -15,6 +15,13 @@ module QA p.description = 'Project with Auto Devops' end + # Disable code_quality check in Auto DevOps pipeline as it takes + # too long and times out the test + Factory::Resource::SecretVariable.fabricate! do |resource| + resource.key = 'CODE_QUALITY_DISABLED' + resource.value = '1' + end + # Create Auto Devops compatible repo Factory::Repository::ProjectPush.fabricate! do |push| push.project = project diff --git a/qa/qa/specs/features/project/create_issue_spec.rb b/qa/qa/specs/features/project/create_issue_spec.rb index 6c62a1026b2..eee7d01a4c8 100644 --- a/qa/qa/specs/features/project/create_issue_spec.rb +++ b/qa/qa/specs/features/project/create_issue_spec.rb @@ -1,5 +1,5 @@ module QA - describe 'creates issue', :core do + describe 'creates issue', :smoke do let(:issue_title) { 'issue title' } def create_issue diff --git a/qa/qa/specs/features/project/create_spec.rb b/qa/qa/specs/features/project/create_spec.rb index 14ecd464685..5e19e490778 100644 --- a/qa/qa/specs/features/project/create_spec.rb +++ b/qa/qa/specs/features/project/create_spec.rb @@ -1,5 +1,5 @@ module QA - describe 'create a new project', :core do + describe 'create a new project', :smoke do it 'user creates a new project' do Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } diff --git a/qa/qa/specs/features/project/deploy_key_clone_spec.rb b/qa/qa/specs/features/project/deploy_key_clone_spec.rb index 054f49b408a..1d099508c24 100644 --- a/qa/qa/specs/features/project/deploy_key_clone_spec.rb +++ b/qa/qa/specs/features/project/deploy_key_clone_spec.rb @@ -1,7 +1,7 @@ require 'digest/sha1' module QA - describe 'cloning code using a deploy key', :core, :docker do + describe 'cloning code using a deploy key', :docker do def login Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } diff --git a/qa/qa/specs/features/project/fork_project_spec.rb b/qa/qa/specs/features/project/fork_project_spec.rb index 8ad0120305a..280978bb950 100644 --- a/qa/qa/specs/features/project/fork_project_spec.rb +++ b/qa/qa/specs/features/project/fork_project_spec.rb @@ -1,5 +1,5 @@ module QA - describe 'Project fork', :core do + describe 'Project fork' do it 'can submit merge requests to upstream master' do Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } @@ -8,14 +8,12 @@ module QA merge_request.fork_branch = 'feature-branch' end - Page::Menu::Main.act { sign_out } - Page::Main::Login.act do - switch_to_sign_in_tab - sign_in_using_credentials - end + Page::Menu::Main.perform { |main| main.sign_out } + Page::Main::Login.perform { |login| login.sign_in_using_credentials } merge_request.visit! - Page::MergeRequest::Show.act { merge! } + + Page::MergeRequest::Show.perform { |show| show.merge! } expect(page).to have_content('The changes were merged') end diff --git a/qa/qa/specs/features/project/import_from_github_spec.rb b/qa/qa/specs/features/project/import_from_github_spec.rb index 221b5c27fba..57695d2c726 100644 --- a/qa/qa/specs/features/project/import_from_github_spec.rb +++ b/qa/qa/specs/features/project/import_from_github_spec.rb @@ -1,5 +1,5 @@ module QA - describe 'user imports a GitHub repo', :core, :github do + describe 'user imports a GitHub repo', :orchestrated, :github do let(:imported_project) do Factory::Resource::ProjectImportedFromGithub.fabricate! do |project| project.name = 'imported-project' diff --git a/qa/qa/specs/features/project/pipelines_spec.rb b/qa/qa/specs/features/project/pipelines_spec.rb index ddedde7a8bc..6c6b4e80626 100644 --- a/qa/qa/specs/features/project/pipelines_spec.rb +++ b/qa/qa/specs/features/project/pipelines_spec.rb @@ -1,5 +1,5 @@ module QA - describe 'CI/CD Pipelines', :core, :docker do + describe 'CI/CD Pipelines', :orchestrated, :docker do let(:executor) { "qa-runner-#{Time.now.to_i}" } after do diff --git a/qa/qa/specs/features/project/wikis_spec.rb b/qa/qa/specs/features/project/wikis_spec.rb index 59cc455fffc..9af2dbd1264 100644 --- a/qa/qa/specs/features/project/wikis_spec.rb +++ b/qa/qa/specs/features/project/wikis_spec.rb @@ -1,5 +1,5 @@ module QA - describe 'Wiki Functionality', :core do + describe 'Wiki Functionality' do def login Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } diff --git a/qa/qa/specs/features/repository/clone_spec.rb b/qa/qa/specs/features/repository/clone_spec.rb index a04ce4e44d9..8b0613c5f78 100644 --- a/qa/qa/specs/features/repository/clone_spec.rb +++ b/qa/qa/specs/features/repository/clone_spec.rb @@ -1,5 +1,5 @@ module QA - describe 'clone code from the repository', :core do + describe 'clone code from the repository' do context 'with regular account over http' do let(:location) do Page::Project::Show.act do diff --git a/qa/qa/specs/features/repository/protected_branches_spec.rb b/qa/qa/specs/features/repository/protected_branches_spec.rb index c2de94516d9..aa23145478d 100644 --- a/qa/qa/specs/features/repository/protected_branches_spec.rb +++ b/qa/qa/specs/features/repository/protected_branches_spec.rb @@ -1,5 +1,5 @@ module QA - describe 'branch protection support', :core do + describe 'branch protection support' do let(:branch_name) { 'protected-branch' } let(:commit_message) { 'Protected push commit message' } let(:project) do diff --git a/qa/qa/specs/features/repository/push_spec.rb b/qa/qa/specs/features/repository/push_spec.rb index fc40b60d915..1e89942e932 100644 --- a/qa/qa/specs/features/repository/push_spec.rb +++ b/qa/qa/specs/features/repository/push_spec.rb @@ -1,5 +1,5 @@ module QA - describe 'push code to repository', :core do + describe 'push code to repository' do context 'with regular account over http' do it 'user pushes code to the repository' do Runtime::Browser.visit(:gitlab, Page::Main::Login) diff --git a/qa/qa/specs/runner.rb b/qa/qa/specs/runner.rb index f8f6fe65599..ccb9d5591de 100644 --- a/qa/qa/specs/runner.rb +++ b/qa/qa/specs/runner.rb @@ -14,7 +14,13 @@ module QA def perform args = [] args.push('--tty') if tty - tags.to_a.each { |tag| args.push(['-t', tag.to_s]) } + + if tags.any? + tags.each { |tag| args.push(['-t', tag.to_s]) } + else + args.push(%w[-t ~orchestrated]) + end + args.push(options) Runtime::Browser.configure! diff --git a/qa/spec/runtime/env_spec.rb b/qa/spec/runtime/env_spec.rb index 851026c71f0..ccc0b906845 100644 --- a/qa/spec/runtime/env_spec.rb +++ b/qa/spec/runtime/env_spec.rb @@ -77,6 +77,31 @@ describe QA::Runtime::Env do end end + describe '.forker?' do + it 'returns false if no forker credentials are defined' do + expect(described_class).not_to be_forker + end + + it 'returns false if only forker username is defined' do + stub_env('GITLAB_FORKER_USERNAME', 'foo') + + expect(described_class).not_to be_forker + end + + it 'returns false if only forker password is defined' do + stub_env('GITLAB_FORKER_PASSWORD', 'bar') + + expect(described_class).not_to be_forker + end + + it 'returns true if forker username and password are defined' do + stub_env('GITLAB_FORKER_USERNAME', 'foo') + stub_env('GITLAB_FORKER_PASSWORD', 'bar') + + expect(described_class).to be_forker + end + end + describe '.github_access_token' do it 'returns "" if GITHUB_ACCESS_TOKEN is not defined' do expect(described_class.github_access_token).to eq('') diff --git a/qa/spec/scenario/test/instance_spec.rb b/qa/spec/scenario/test/instance/all_spec.rb index 0d0b534911f..423527e938e 100644 --- a/qa/spec/scenario/test/instance_spec.rb +++ b/qa/spec/scenario/test/instance/all_spec.rb @@ -1,10 +1,4 @@ -describe QA::Scenario::Test::Instance do - subject do - Class.new(described_class) do - tags :rspec - end - end - +describe QA::Scenario::Test::Instance::All do context '#perform' do let(:arguments) { spy('Runtime::Scenario') } let(:release) { spy('Runtime::Release') } @@ -26,16 +20,16 @@ describe QA::Scenario::Test::Instance do end context 'no paths' do - it 'should call runner with default arguments' do + it 'calls runner with default arguments' do subject.perform("test") expect(runner).to have_received(:options=) - .with(::File.expand_path('../../../qa/specs/features', __dir__)) + .with(::File.expand_path('../../../../qa/specs/features', __dir__)) end end context 'specifying paths' do - it 'should call runner with paths' do + it 'calls runner with paths' do subject.perform('test', 'path1', 'path2') expect(runner).to have_received(:options=).with(%w[path1 path2]) diff --git a/qa/spec/scenario/test/instance/smoke_spec.rb b/qa/spec/scenario/test/instance/smoke_spec.rb new file mode 100644 index 00000000000..e79d19e8212 --- /dev/null +++ b/qa/spec/scenario/test/instance/smoke_spec.rb @@ -0,0 +1,45 @@ +describe QA::Scenario::Test::Instance::Smoke do + subject { Class.new(described_class) { tags :smoke } } + + context '#perform' do + let(:arguments) { spy('Runtime::Scenario') } + let(:release) { spy('Runtime::Release') } + let(:runner) { spy('Specs::Runner') } + + before do + stub_const('QA::Runtime::Release', release) + stub_const('QA::Runtime::Scenario', arguments) + stub_const('QA::Specs::Runner', runner) + + allow(runner).to receive(:perform).and_yield(runner) + end + + it 'sets an address of the subject' do + subject.perform("hello") + + expect(arguments).to have_received(:define) + .with(:gitlab_address, "hello") + end + + it 'has a smoke tag' do + expect(subject.focus).to eq([:smoke]) # rubocop:disable Focus + end + + context 'no paths' do + it 'calls runner with default arguments' do + subject.perform("test") + + expect(runner).to have_received(:options=) + .with(::File.expand_path('../../../../qa/specs/features', __dir__)) + end + end + + context 'specifying paths' do + it 'calls runner with paths' do + subject.perform('test', 'path1', 'path2') + + expect(runner).to have_received(:options=).with(%w[path1 path2]) + end + end + end +end diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb index 2c59d1929a1..883bb35f396 100644 --- a/spec/controllers/autocomplete_controller_spec.rb +++ b/spec/controllers/autocomplete_controller_spec.rb @@ -274,14 +274,11 @@ describe AutocompleteController do context 'authorized projects apply limit' do before do - authorized_project2 = create(:project) - authorized_project3 = create(:project) - - authorized_project.add_maintainer(user) - authorized_project2.add_maintainer(user) - authorized_project3.add_maintainer(user) + allow(Kaminari.config).to receive(:default_per_page).and_return(2) - stub_const 'MoveToProjectFinder::PAGE_SIZE', 2 + create_list(:project, 2) do |project| + project.add_maintainer(user) + end end describe 'GET #projects with project ID' do @@ -291,7 +288,7 @@ describe AutocompleteController do it 'returns projects' do expect(json_response).to be_kind_of(Array) - expect(json_response.size).to eq 2 # Of a total of 3 + expect(json_response.size).to eq(Kaminari.config.default_per_page) end end end diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb index be8754a5315..5623e47eadf 100644 --- a/spec/features/admin/admin_runners_spec.rb +++ b/spec/features/admin/admin_runners_spec.rb @@ -20,7 +20,7 @@ describe "Admin Runners" do it 'has all necessary texts' do expect(page).to have_text "Setup a shared Runner manually" - expect(page).to have_text "Runners with last contact more than a minute ago: 1" + expect(page).to have_text "Runners currently online: 1" end describe 'search' do @@ -55,7 +55,7 @@ describe "Admin Runners" do it 'has all necessary texts including no runner message' do expect(page).to have_text "Setup a shared Runner manually" - expect(page).to have_text "Runners with last contact more than a minute ago: 0" + expect(page).to have_text "Runners currently online: 0" expect(page).to have_text 'No runners found' end end diff --git a/spec/finders/autocomplete/group_finder_spec.rb b/spec/finders/autocomplete/group_finder_spec.rb new file mode 100644 index 00000000000..d7cb2c3bbe2 --- /dev/null +++ b/spec/finders/autocomplete/group_finder_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Autocomplete::GroupFinder do + let(:user) { create(:user) } + + describe '#execute' do + context 'with a project' do + it 'returns nil' do + project = create(:project) + + expect(described_class.new(user, project).execute).to be_nil + end + end + + context 'without a group ID' do + it 'returns nil' do + expect(described_class.new(user).execute).to be_nil + end + end + + context 'with an empty String as the group ID' do + it 'returns nil' do + expect(described_class.new(user, nil, group_id: '').execute).to be_nil + end + end + + context 'without a project and with a group ID' do + it 'raises ActiveRecord::RecordNotFound if the group does not exist' do + finder = described_class.new(user, nil, group_id: 1) + + expect { finder.execute }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'raises ActiveRecord::RecordNotFound if the user can not read the group' do + group = create(:group, :private) + finder = described_class.new(user, nil, group_id: group.id) + + expect { finder.execute }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'raises ActiveRecord::RecordNotFound if an anonymous user can not read the group' do + group = create(:group, :private) + finder = described_class.new(nil, nil, group_id: group.id) + + expect { finder.execute }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'returns the group if it exists and is readable' do + group = create(:group) + finder = described_class.new(user, nil, group_id: group.id) + + expect(finder.execute).to eq(group) + end + end + end +end diff --git a/spec/finders/move_to_project_finder_spec.rb b/spec/finders/autocomplete/move_to_project_finder_spec.rb index 1b3f44cced1..c3bc410a7f6 100644 --- a/spec/finders/move_to_project_finder_spec.rb +++ b/spec/finders/autocomplete/move_to_project_finder_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe MoveToProjectFinder do +describe Autocomplete::MoveToProjectFinder do let(:user) { create(:user) } let(:project) { create(:project) } @@ -10,14 +10,14 @@ describe MoveToProjectFinder do let(:developer_project) { create(:project) } let(:maintainer_project) { create(:project) } - subject { described_class.new(user) } - describe '#execute' do context 'filter' do it 'does not return projects under Gitlab::Access::REPORTER' do guest_project.add_guest(user) - expect(subject.execute(project)).to be_empty + finder = described_class.new(user, project_id: project.id) + + expect(finder.execute).to be_empty end it 'returns projects equal or above Gitlab::Access::REPORTER ordered by id in descending order' do @@ -25,13 +25,17 @@ describe MoveToProjectFinder do developer_project.add_developer(user) maintainer_project.add_maintainer(user) - expect(subject.execute(project).to_a).to eq([maintainer_project, developer_project, reporter_project]) + finder = described_class.new(user, project_id: project.id) + + expect(finder.execute.to_a).to eq([maintainer_project, developer_project, reporter_project]) end it 'does not include the source project' do project.add_reporter(user) - expect(subject.execute(project).to_a).to be_empty + finder = described_class.new(user, project_id: project.id) + + expect(finder.execute.to_a).to be_empty end it 'does not return archived projects' do @@ -40,7 +44,9 @@ describe MoveToProjectFinder do other_reporter_project = create(:project) other_reporter_project.add_reporter(user) - expect(subject.execute(project).to_a).to eq([other_reporter_project]) + finder = described_class.new(user, project_id: project.id) + + expect(finder.execute.to_a).to eq([other_reporter_project]) end it 'does not return projects for which issues are disabled' do @@ -49,39 +55,42 @@ describe MoveToProjectFinder do other_reporter_project = create(:project) other_reporter_project.add_reporter(user) - expect(subject.execute(project).to_a).to eq([other_reporter_project]) + finder = described_class.new(user, project_id: project.id) + + expect(finder.execute.to_a).to eq([other_reporter_project]) end it 'returns a page of projects ordered by id in descending order' do - stub_const 'MoveToProjectFinder::PAGE_SIZE', 2 + allow(Kaminari.config).to receive(:default_per_page).and_return(2) - reporter_project.add_reporter(user) - developer_project.add_developer(user) - maintainer_project.add_maintainer(user) + projects = create_list(:project, 2) do |project| + project.add_developer(user) + end - expect(subject.execute(project).to_a).to eq([maintainer_project, developer_project]) + finder = described_class.new(user, project_id: project.id) + page = finder.execute.to_a + + expect(page.length).to eq(Kaminari.config.default_per_page) + expect(page[0]).to eq(projects.last) end it 'returns projects after the given offset id' do - stub_const 'MoveToProjectFinder::PAGE_SIZE', 2 - reporter_project.add_reporter(user) developer_project.add_developer(user) maintainer_project.add_maintainer(user) - expect(subject.execute(project, search: nil, offset_id: maintainer_project.id).to_a).to eq([developer_project, reporter_project]) - expect(subject.execute(project, search: nil, offset_id: developer_project.id).to_a).to eq([reporter_project]) - expect(subject.execute(project, search: nil, offset_id: reporter_project.id).to_a).to be_empty - end - end + expect(described_class.new(user, project_id: project.id, offset_id: maintainer_project.id).execute.to_a) + .to eq([developer_project, reporter_project]) - context 'search' do - it 'uses Project#search' do - expect(user).to receive_message_chain(:projects_where_can_admin_issues, :search) { Project.all } + expect(described_class.new(user, project_id: project.id, offset_id: developer_project.id).execute.to_a) + .to eq([reporter_project]) - subject.execute(project, search: 'wadus') + expect(described_class.new(user, project_id: project.id, offset_id: reporter_project.id).execute.to_a) + .to be_empty end + end + context 'search' do it 'returns projects matching a search query' do foo_project = create(:project) foo_project.add_maintainer(user) @@ -89,8 +98,11 @@ describe MoveToProjectFinder do wadus_project = create(:project, name: 'wadus') wadus_project.add_maintainer(user) - expect(subject.execute(project).to_a).to eq([wadus_project, foo_project]) - expect(subject.execute(project, search: 'wadus').to_a).to eq([wadus_project]) + expect(described_class.new(user, project_id: project.id).execute.to_a) + .to eq([wadus_project, foo_project]) + + expect(described_class.new(user, project_id: project.id, search: 'wadus').execute.to_a) + .to eq([wadus_project]) end end end diff --git a/spec/finders/autocomplete/project_finder_spec.rb b/spec/finders/autocomplete/project_finder_spec.rb new file mode 100644 index 00000000000..207d0598c28 --- /dev/null +++ b/spec/finders/autocomplete/project_finder_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Autocomplete::ProjectFinder do + let(:user) { create(:user) } + + describe '#execute' do + context 'without a project ID' do + it 'returns nil' do + expect(described_class.new(user).execute).to be_nil + end + end + + context 'with an empty String as the project ID' do + it 'returns nil' do + expect(described_class.new(user, project_id: '').execute).to be_nil + end + end + + context 'with a project ID' do + it 'raises ActiveRecord::RecordNotFound if the project does not exist' do + finder = described_class.new(user, project_id: 1) + + expect { finder.execute }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'raises ActiveRecord::RecordNotFound if the user can not read the project' do + project = create(:project, :private) + + finder = described_class.new(user, project_id: project.id) + + expect { finder.execute }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'raises ActiveRecord::RecordNotFound if an anonymous user can not read the project' do + project = create(:project, :private) + + finder = described_class.new(nil, project_id: project.id) + + expect { finder.execute }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'returns the project if it exists and is readable' do + project = create(:project, :private) + + project.add_maintainer(user) + + finder = described_class.new(user, project_id: project.id) + + expect(finder.execute).to eq(project) + end + end + end +end diff --git a/spec/finders/autocomplete_users_finder_spec.rb b/spec/finders/autocomplete/users_finder_spec.rb index dcf9111776e..abd0d6b5185 100644 --- a/spec/finders/autocomplete_users_finder_spec.rb +++ b/spec/finders/autocomplete/users_finder_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe AutocompleteUsersFinder do +describe Autocomplete::UsersFinder do describe '#execute' do let!(:user1) { create(:user, username: 'johndoe') } let!(:user2) { create(:user, :blocked, username: 'notsorandom') } diff --git a/spec/finders/awarded_emoji_finder_spec.rb b/spec/finders/awarded_emoji_finder_spec.rb new file mode 100644 index 00000000000..d4479df7418 --- /dev/null +++ b/spec/finders/awarded_emoji_finder_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe AwardedEmojiFinder do + describe '#execute' do + it 'returns an Array containing the awarded emoji names' do + user = create(:user) + + create(:award_emoji, user: user, name: 'thumbsup') + create(:award_emoji, user: user, name: 'thumbsup') + create(:award_emoji, user: user, name: 'thumbsdown') + + awarded = described_class.new(user).execute + + expect(awarded).to eq([{ name: 'thumbsup' }, { name: 'thumbsdown' }]) + end + + it 'returns an empty Array when no user is given' do + awarded = described_class.new.execute + + expect(awarded).to be_empty + end + end +end diff --git a/spec/finders/user_finder_spec.rb b/spec/finders/user_finder_spec.rb new file mode 100644 index 00000000000..e53aa50dd33 --- /dev/null +++ b/spec/finders/user_finder_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe UserFinder do + describe '#execute' do + context 'when the user exists' do + it 'returns the user' do + user = create(:user) + found = described_class.new(id: user.id).execute + + expect(found).to eq(user) + end + end + + context 'when the user does not exist' do + it 'returns nil' do + found = described_class.new(id: 1).execute + + expect(found).to be_nil + end + end + end + + describe '#execute!' do + context 'when the user exists' do + it 'returns the user' do + user = create(:user) + found = described_class.new(id: user.id).execute! + + expect(found).to eq(user) + end + end + + context 'when the user does not exist' do + it 'raises ActiveRecord::RecordNotFound' do + finder = described_class.new(id: 1) + + expect { finder.execute! }.to raise_error(ActiveRecord::RecordNotFound) + end + end + end +end diff --git a/spec/lib/banzai/filter/spaced_link_filter_spec.rb b/spec/lib/banzai/filter/spaced_link_filter_spec.rb new file mode 100644 index 00000000000..4463c011522 --- /dev/null +++ b/spec/lib/banzai/filter/spaced_link_filter_spec.rb @@ -0,0 +1,66 @@ +require 'spec_helper' + +describe Banzai::Filter::SpacedLinkFilter do + include FilterSpecHelper + + let(:link) { '[example](page slug)' } + + it 'converts slug with spaces to a link' do + doc = filter("See #{link}") + + expect(doc.at_css('a').text).to eq 'example' + expect(doc.at_css('a')['href']).to eq 'page%20slug' + expect(doc.at_css('p')).to eq nil + end + + it 'converts slug with spaces and a title to a link' do + link = '[example](page slug "title")' + doc = filter("See #{link}") + + expect(doc.at_css('a').text).to eq 'example' + expect(doc.at_css('a')['href']).to eq 'page%20slug' + expect(doc.at_css('a')['title']).to eq 'title' + expect(doc.at_css('p')).to eq nil + end + + it 'does nothing when markdown_engine is redcarpet' do + exp = act = link + expect(filter(act, markdown_engine: :redcarpet).to_html).to eq exp + end + + it 'does nothing with empty text' do + link = '[](page slug)' + doc = filter("See #{link}") + + expect(doc.at_css('a')).to eq nil + end + + it 'does nothing with an empty slug' do + link = '[example]()' + doc = filter("See #{link}") + + expect(doc.at_css('a')).to eq nil + end + + it 'converts multiple URLs' do + link1 = '[first](slug one)' + link2 = '[second](http://example.com/slug two)' + doc = filter("See #{link1} and #{link2}") + + found_links = doc.css('a') + + expect(found_links.size).to eq(2) + expect(found_links[0].text).to eq 'first' + expect(found_links[0]['href']).to eq 'slug%20one' + expect(found_links[1].text).to eq 'second' + expect(found_links[1]['href']).to eq 'http://example.com/slug%20two' + end + + described_class::IGNORE_PARENTS.each do |elem| + it "ignores valid links contained inside '#{elem}' element" do + exp = act = "<#{elem}>See #{link}</#{elem}>" + + expect(filter(act).to_html).to eq exp + end + end +end diff --git a/spec/lib/gitlab/data_builder/build_spec.rb b/spec/lib/gitlab/data_builder/build_spec.rb index ee91decafad..14fe196a986 100644 --- a/spec/lib/gitlab/data_builder/build_spec.rb +++ b/spec/lib/gitlab/data_builder/build_spec.rb @@ -15,6 +15,7 @@ describe Gitlab::DataBuilder::Build do it { expect(data[:build_id]).to eq(build.id) } it { expect(data[:build_status]).to eq(build.status) } it { expect(data[:build_allow_failure]).to eq(false) } + it { expect(data[:build_failure_reason]).to eq(build.failure_reason) } it { expect(data[:project_id]).to eq(build.project.id) } it { expect(data[:project_name]).to eq(build.project.full_name) } diff --git a/spec/lib/gitlab/gitaly_client/diff_spec.rb b/spec/lib/gitlab/gitaly_client/diff_spec.rb index 00a31ac0b96..ec7ab2fdedb 100644 --- a/spec/lib/gitlab/gitaly_client/diff_spec.rb +++ b/spec/lib/gitlab/gitaly_client/diff_spec.rb @@ -9,7 +9,9 @@ describe Gitlab::GitalyClient::Diff do new_mode: 0100644, from_id: '357406f3075a57708d0163752905cc1576fceacc', to_id: '8e5177d718c561d36efde08bad36b43687ee6bf0', - patch: 'a' * 100 + patch: 'a' * 100, + collapsed: false, + too_large: false } end @@ -22,6 +24,8 @@ describe Gitlab::GitalyClient::Diff do it { is_expected.to respond_to(:from_id) } it { is_expected.to respond_to(:to_id) } it { is_expected.to respond_to(:patch) } + it { is_expected.to respond_to(:collapsed) } + it { is_expected.to respond_to(:too_large) } describe '#==' do it { expect(subject).to eq(described_class.new(diff_fields)) } diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index e9a1932407d..b4269bd5786 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -210,7 +210,6 @@ project: - flowdock_service - assembla_service - asana_service -- gemnasium_service - slack_service - microsoft_teams_service - mattermost_service diff --git a/spec/models/award_emoji_spec.rb b/spec/models/award_emoji_spec.rb index b909e04dfc3..3f52091698c 100644 --- a/spec/models/award_emoji_spec.rb +++ b/spec/models/award_emoji_spec.rb @@ -77,4 +77,27 @@ describe AwardEmoji do end end end + + describe '.award_counts_for_user' do + let(:user) { create(:user) } + + before do + create(:award_emoji, user: user, name: 'thumbsup') + create(:award_emoji, user: user, name: 'thumbsup') + create(:award_emoji, user: user, name: 'thumbsdown') + create(:award_emoji, user: user, name: '+1') + end + + it 'returns the awarded emoji in descending order' do + awards = described_class.award_counts_for_user(user) + + expect(awards).to eq('thumbsup' => 2, 'thumbsdown' => 1, '+1' => 1) + end + + it 'limits the returned number of rows' do + awards = described_class.award_counts_for_user(user, 1) + + expect(awards).to eq('thumbsup' => 2) + end + end end diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb index 7454be3ab2f..26b75c75e1d 100644 --- a/spec/models/clusters/applications/prometheus_spec.rb +++ b/spec/models/clusters/applications/prometheus_spec.rb @@ -154,20 +154,17 @@ describe Clusters::Applications::Prometheus do end describe '#install_command' do - let(:kubeclient) { double('kubernetes client') } let(:prometheus) { create(:clusters_applications_prometheus) } - it 'returns an instance of Gitlab::Kubernetes::Helm::InstallCommand' do - expect(prometheus.install_command).to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand) - end + subject { prometheus.install_command } - it 'should be initialized with 3 arguments' do - command = prometheus.install_command + it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand) } - expect(command.name).to eq('prometheus') - expect(command.chart).to eq('stable/prometheus') - expect(command.version).to eq('6.7.3') - expect(command.files).to eq(prometheus.files) + it 'should be initialized with 3 arguments' do + expect(subject.name).to eq('prometheus') + expect(subject.chart).to eq('stable/prometheus') + expect(subject.version).to eq('6.7.3') + expect(subject.files).to eq(prometheus.files) end context 'application failed to install previously' do diff --git a/spec/models/concerns/optionally_search_spec.rb b/spec/models/concerns/optionally_search_spec.rb new file mode 100644 index 00000000000..ff4212ddf18 --- /dev/null +++ b/spec/models/concerns/optionally_search_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe OptionallySearch do + let(:model) do + Class.new(ActiveRecord::Base) do + self.table_name = 'users' + + include OptionallySearch + end + end + + describe '.search' do + it 'raises NotImplementedError' do + expect { model.search('foo') }.to raise_error(NotImplementedError) + end + end + + describe '.optionally_search' do + context 'when a query is given' do + it 'delegates to the search method' do + expect(model) + .to receive(:search) + .with('foo') + + model.optionally_search('foo') + end + end + + context 'when no query is given' do + it 'returns the current relation' do + expect(model.optionally_search).to be_a_kind_of(ActiveRecord::Relation) + end + end + + context 'when an empty query is given' do + it 'returns the current relation' do + expect(model.optionally_search('')) + .to be_a_kind_of(ActiveRecord::Relation) + end + end + end +end diff --git a/spec/models/project_services/gemnasium_service_spec.rb b/spec/models/project_services/gemnasium_service_spec.rb deleted file mode 100644 index 6e417323e40..00000000000 --- a/spec/models/project_services/gemnasium_service_spec.rb +++ /dev/null @@ -1,74 +0,0 @@ -require 'spec_helper' - -describe GemnasiumService do - describe "Associations" do - it { is_expected.to belong_to :project } - it { is_expected.to have_one :service_hook } - end - - describe 'Validations' do - context 'when service is active' do - before do - subject.active = true - end - - it { is_expected.to validate_presence_of(:token) } - it { is_expected.to validate_presence_of(:api_key) } - end - - context 'when service is inactive' do - before do - subject.active = false - end - - it { is_expected.not_to validate_presence_of(:token) } - it { is_expected.not_to validate_presence_of(:api_key) } - end - end - - describe "deprecated?" do - let(:project) { create(:project, :repository) } - let(:gemnasium_service) { described_class.new } - - before do - allow(gemnasium_service).to receive_messages( - project_id: project.id, - project: project, - service_hook: true, - token: 'verySecret', - api_key: 'GemnasiumUserApiKey' - ) - end - - it "is true" do - expect(gemnasium_service.deprecated?).to be true - end - - it "can't create a new service" do - expect(gemnasium_service.save).to be false - expect(gemnasium_service.errors[:base].first) - .to eq('Gemnasium has been acquired by GitLab in January 2018. Since May 15, 2018, the service provided by Gemnasium is no longer available.') - end - end - - describe "Execute" do - let(:user) { create(:user) } - let(:project) { create(:project, :repository) } - let(:gemnasium_service) { described_class.new } - let(:sample_data) { Gitlab::DataBuilder::Push.build_sample(project, user) } - - before do - allow(gemnasium_service).to receive_messages( - project_id: project.id, - project: project, - service_hook: true, - token: 'verySecret', - api_key: 'GemnasiumUserApiKey' - ) - end - it "calls Gemnasium service" do - expect(Gemnasium::GitlabService).to receive(:execute).with(an_instance_of(Hash)).once - gemnasium_service.execute(sample_data) - end - end -end diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb index ac9ff59b9b5..54f1a0e38a5 100644 --- a/spec/models/project_services/jira_service_spec.rb +++ b/spec/models/project_services/jira_service_spec.rb @@ -27,13 +27,13 @@ describe JiraService do end end - describe "Associations" do + describe 'Associations' do it { is_expected.to belong_to :project } it { is_expected.to have_one :service_hook } it { is_expected.to allow_value(nil).for(:jira_issue_transition_id) } - it { is_expected.to allow_value("1,2,3").for(:jira_issue_transition_id) } - it { is_expected.to allow_value("1;2;3").for(:jira_issue_transition_id) } - it { is_expected.not_to allow_value("a,b,cd").for(:jira_issue_transition_id) } + it { is_expected.to allow_value('1,2,3').for(:jira_issue_transition_id) } + it { is_expected.to allow_value('1;2;3').for(:jira_issue_transition_id) } + it { is_expected.not_to allow_value('a,b,cd').for(:jira_issue_transition_id) } end describe 'Validations' do @@ -116,143 +116,142 @@ describe JiraService do describe '#close_issue' do let(:custom_base_url) { 'http://custom_url' } let(:user) { create(:user) } - let(:project) { create(:project) } - let(:merge_request) { create(:merge_request) } - - before do - @jira_service = described_class.new - allow(@jira_service).to receive_messages( - project_id: project.id, - project: project, - service_hook: true, - url: 'http://jira.example.com', - username: 'gitlab_jira_username', - password: 'gitlab_jira_password', - jira_issue_transition_id: "999" - ) + let(:project) { create(:project, :repository) } - # These stubs are needed to test JiraService#close_issue. - # We close the issue then do another request to API to check if it got closed. - # Here is stubbed the API return with a closed and an opened issues. - open_issue = JIRA::Resource::Issue.new(@jira_service.client, attrs: { "id" => "JIRA-123" }) - closed_issue = open_issue.dup - allow(open_issue).to receive(:resolution).and_return(false) - allow(closed_issue).to receive(:resolution).and_return(true) - allow(JIRA::Resource::Issue).to receive(:find).and_return(open_issue, closed_issue) - - allow_any_instance_of(JIRA::Resource::Issue).to receive(:key).and_return("JIRA-123") - allow(JIRA::Resource::Remotelink).to receive(:all).and_return([]) - - @jira_service.save - - project_issues_url = 'http://jira.example.com/rest/api/2/issue/JIRA-123' - @transitions_url = 'http://jira.example.com/rest/api/2/issue/JIRA-123/transitions' - @comment_url = 'http://jira.example.com/rest/api/2/issue/JIRA-123/comment' - @remote_link_url = 'http://jira.example.com/rest/api/2/issue/JIRA-123/remotelink' - - WebMock.stub_request(:get, project_issues_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)) - WebMock.stub_request(:post, @transitions_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)) - WebMock.stub_request(:post, @comment_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)) - WebMock.stub_request(:post, @remote_link_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)) - end + shared_examples 'close_issue' do + before do + @jira_service = described_class.new + allow(@jira_service).to receive_messages( + project_id: project.id, + project: project, + service_hook: true, + url: 'http://jira.example.com', + username: 'gitlab_jira_username', + password: 'gitlab_jira_password', + jira_issue_transition_id: '999' + ) - it "calls JIRA API" do - @jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project)) + # These stubs are needed to test JiraService#close_issue. + # We close the issue then do another request to API to check if it got closed. + # Here is stubbed the API return with a closed and an opened issues. + open_issue = JIRA::Resource::Issue.new(@jira_service.client, attrs: { 'id' => 'JIRA-123' }) + closed_issue = open_issue.dup + allow(open_issue).to receive(:resolution).and_return(false) + allow(closed_issue).to receive(:resolution).and_return(true) + allow(JIRA::Resource::Issue).to receive(:find).and_return(open_issue, closed_issue) - expect(WebMock).to have_requested(:post, @comment_url).with( - body: /Issue solved with/ - ).once - end + allow_any_instance_of(JIRA::Resource::Issue).to receive(:key).and_return('JIRA-123') + allow(JIRA::Resource::Remotelink).to receive(:all).and_return([]) - # Check https://developer.atlassian.com/jiradev/jira-platform/guides/other/guide-jira-remote-issue-links/fields-in-remote-issue-links - # for more information - it "creates Remote Link reference in JIRA for comment" do - @jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project)) - - favicon_path = "http://localhost/assets/#{find_asset('favicon.png').digest_path}" - - # Creates comment - expect(WebMock).to have_requested(:post, @comment_url) - # Creates Remote Link in JIRA issue fields - expect(WebMock).to have_requested(:post, @remote_link_url).with( - body: hash_including( - GlobalID: "GitLab", - object: { - url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/commit/#{merge_request.diff_head_sha}", - title: "GitLab: Solved by commit #{merge_request.diff_head_sha}.", - icon: { title: "GitLab", url16x16: favicon_path }, - status: { resolved: true } - } - ) - ).once - end + @jira_service.save - it "does not send comment or remote links to issues already closed" do - allow_any_instance_of(JIRA::Resource::Issue).to receive(:resolution).and_return(true) + project_issues_url = 'http://jira.example.com/rest/api/2/issue/JIRA-123' + @transitions_url = 'http://jira.example.com/rest/api/2/issue/JIRA-123/transitions' + @comment_url = 'http://jira.example.com/rest/api/2/issue/JIRA-123/comment' + @remote_link_url = 'http://jira.example.com/rest/api/2/issue/JIRA-123/remotelink' - @jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project)) + WebMock.stub_request(:get, project_issues_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)) + WebMock.stub_request(:post, @transitions_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)) + WebMock.stub_request(:post, @comment_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)) + WebMock.stub_request(:post, @remote_link_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)) + end - expect(WebMock).not_to have_requested(:post, @comment_url) - expect(WebMock).not_to have_requested(:post, @remote_link_url) - end + it 'calls JIRA API' do + @jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project)) - it "does not send comment or remote links to issues with unknown resolution" do - allow_any_instance_of(JIRA::Resource::Issue).to receive(:respond_to?).with(:resolution).and_return(false) + expect(WebMock).to have_requested(:post, @comment_url).with( + body: /Issue solved with/ + ).once + end - @jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project)) + # Check https://developer.atlassian.com/jiradev/jira-platform/guides/other/guide-jira-remote-issue-links/fields-in-remote-issue-links + # for more information + it 'creates Remote Link reference in JIRA for comment' do + @jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project)) + + favicon_path = "http://localhost/assets/#{find_asset('favicon.png').digest_path}" + + # Creates comment + expect(WebMock).to have_requested(:post, @comment_url) + # Creates Remote Link in JIRA issue fields + expect(WebMock).to have_requested(:post, @remote_link_url).with( + body: hash_including( + GlobalID: 'GitLab', + object: { + url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/commit/#{commit_id}", + title: "GitLab: Solved by commit #{commit_id}.", + icon: { title: 'GitLab', url16x16: favicon_path }, + status: { resolved: true } + } + ) + ).once + end - expect(WebMock).not_to have_requested(:post, @comment_url) - expect(WebMock).not_to have_requested(:post, @remote_link_url) - end + it 'does not send comment or remote links to issues already closed' do + allow_any_instance_of(JIRA::Resource::Issue).to receive(:resolution).and_return(true) - it "references the GitLab commit/merge request" do - stub_config_setting(base_url: custom_base_url) + @jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project)) - @jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project)) + expect(WebMock).not_to have_requested(:post, @comment_url) + expect(WebMock).not_to have_requested(:post, @remote_link_url) + end - expect(WebMock).to have_requested(:post, @comment_url).with( - body: %r{#{custom_base_url}/#{project.full_path}/commit/#{merge_request.diff_head_sha}} - ).once - end + it 'does not send comment or remote links to issues with unknown resolution' do + allow_any_instance_of(JIRA::Resource::Issue).to receive(:respond_to?).with(:resolution).and_return(false) - it "references the GitLab commit/merge request (relative URL)" do - stub_config_setting(relative_url_root: '/gitlab') - stub_config_setting(url: Settings.send(:build_gitlab_url)) + @jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project)) - allow(described_class).to receive(:default_url_options) do - { script_name: '/gitlab' } + expect(WebMock).not_to have_requested(:post, @comment_url) + expect(WebMock).not_to have_requested(:post, @remote_link_url) end - @jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project)) + it 'references the GitLab commit' do + stub_config_setting(base_url: custom_base_url) - expect(WebMock).to have_requested(:post, @comment_url).with( - body: %r{#{Gitlab.config.gitlab.url}/#{project.full_path}/commit/#{merge_request.diff_head_sha}} - ).once - end + @jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project)) + + expect(WebMock).to have_requested(:post, @comment_url).with( + body: %r{#{custom_base_url}/#{project.full_path}/commit/#{commit_id}} + ).once + end + + it 'references the GitLab commit' do + stub_config_setting(relative_url_root: '/gitlab') + stub_config_setting(url: Settings.send(:build_gitlab_url)) + + allow(described_class).to receive(:default_url_options) do + { script_name: '/gitlab' } + end + + @jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project)) - context '#close_issue' do - it "logs exception when transition id is not valid" do + expect(WebMock).to have_requested(:post, @comment_url).with( + body: %r{#{Gitlab.config.gitlab.url}/#{project.full_path}/commit/#{commit_id}} + ).once + end + + it 'logs exception when transition id is not valid' do allow(Rails.logger).to receive(:info) - WebMock.stub_request(:post, @transitions_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)).and_raise("Bad Request") + WebMock.stub_request(:post, @transitions_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)).and_raise('Bad Request') - @jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project)) + @jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project)) - expect(Rails.logger).to have_received(:info).with("JiraService Issue Transition failed message ERROR: http://jira.example.com - Bad Request") + expect(Rails.logger).to have_received(:info).with('JiraService Issue Transition failed message ERROR: http://jira.example.com - Bad Request') end - it "calls the api with jira_issue_transition_id" do - @jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project)) + it 'calls the api with jira_issue_transition_id' do + @jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project)) expect(WebMock).to have_requested(:post, @transitions_url).with( body: /999/ ).once end - context "when have multiple transition ids" do - it "calls the api with transition ids separated by comma" do - allow(@jira_service).to receive_messages(jira_issue_transition_id: "1,2,3") + context 'when have multiple transition ids' do + it 'calls the api with transition ids separated by comma' do + allow(@jira_service).to receive_messages(jira_issue_transition_id: '1,2,3') - @jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project)) + @jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project)) 1.upto(3) do |transition_id| expect(WebMock).to have_requested(:post, @transitions_url).with( @@ -261,10 +260,10 @@ describe JiraService do end end - it "calls the api with transition ids separated by semicolon" do - allow(@jira_service).to receive_messages(jira_issue_transition_id: "1;2;3") + it 'calls the api with transition ids separated by semicolon' do + allow(@jira_service).to receive_messages(jira_issue_transition_id: '1;2;3') - @jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project)) + @jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project)) 1.upto(3) do |transition_id| expect(WebMock).to have_requested(:post, @transitions_url).with( @@ -274,6 +273,20 @@ describe JiraService do end end end + + context 'when resource is a merge request' do + let(:resource) { create(:merge_request) } + let(:commit_id) { resource.diff_head_sha } + + it_behaves_like 'close_issue' + end + + context 'when resource is a commit' do + let(:resource) { project.commit('master') } + let(:commit_id) { resource.id } + + it_behaves_like 'close_issue' + end end describe '#test_settings' do @@ -321,17 +334,17 @@ describe JiraService do end end - describe "Stored password invalidation" do + describe 'Stored password invalidation' do let(:project) { create(:project) } - context "when a password was previously set" do + context 'when a password was previously set' do before do @jira_service = described_class.create!( project: project, properties: { url: 'http://jira.example.com/web', username: 'mic', - password: "password" + password: 'password' } ) end @@ -370,10 +383,10 @@ describe JiraService do @jira_service.url = 'http://jira_edited.example.com/rweb' @jira_service.save - expect(@jira_service.password).to eq("password") + expect(@jira_service.password).to eq('password') end - it 'reset password if api url set to ""' do + it 'reset password if api url set to empty' do @jira_service.api_url = '' @jira_service.save @@ -440,7 +453,7 @@ describe JiraService do it 'is initialized' do expect(@service.title).to eq('JIRA') - expect(@service.description).to eq("Jira issue tracker") + expect(@service.description).to eq('Jira issue tracker') end end @@ -454,7 +467,7 @@ describe JiraService do @service.destroy! end - it "is correct" do + it 'is correct' do expect(@service.title).to eq('Jira One') expect(@service.description).to eq('Jira One issue tracker') end @@ -476,7 +489,7 @@ describe JiraService do it 'is initialized' do expect(@service.options[:use_cookies]).to eq(true) - expect(@service.options[:additional_cookies]).to eq(["OBBasicAuth=fromDialog"]) + expect(@service.options[:additional_cookies]).to eq(['OBBasicAuth=fromDialog']) end end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index d8a5e5f6869..8cb706b54b0 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -42,7 +42,6 @@ describe Project do it { is_expected.to have_one(:assembla_service) } it { is_expected.to have_one(:slack_slash_commands_service) } it { is_expected.to have_one(:mattermost_slash_commands_service) } - it { is_expected.to have_one(:gemnasium_service) } it { is_expected.to have_one(:buildkite_service) } it { is_expected.to have_one(:bamboo_service) } it { is_expected.to have_one(:teamcity_service) } @@ -1478,6 +1477,53 @@ describe Project do end end + describe '.optionally_search' do + let(:project) { create(:project) } + + it 'searches for projects matching the query if one is given' do + relation = described_class.optionally_search(project.name) + + expect(relation).to eq([project]) + end + + it 'returns the current relation if no search query is given' do + relation = described_class.where(id: project.id) + + expect(relation.optionally_search).to eq(relation) + end + end + + describe '.paginate_in_descending_order_using_id' do + let!(:project1) { create(:project) } + let!(:project2) { create(:project) } + + it 'orders the relation in descending order' do + expect(described_class.paginate_in_descending_order_using_id) + .to eq([project2, project1]) + end + + it 'applies a limit to the relation' do + expect(described_class.paginate_in_descending_order_using_id(limit: 1)) + .to eq([project2]) + end + + it 'limits projects by and ID when given' do + expect(described_class.paginate_in_descending_order_using_id(before: project2.id)) + .to eq([project1]) + end + end + + describe '.including_namespace_and_owner' do + it 'eager loads the namespace and namespace owner' do + create(:project) + + row = described_class.eager_load_namespace_and_owner.to_a.first + recorder = ActiveRecord::QueryRecorder.new { row.namespace.owner } + + expect(recorder.count).to be_zero + end + end + describe '#expire_caches_before_rename' do let(:project) { create(:project, :repository) } let(:repo) { double(:repo, exists?: true) } diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index f5e2c977104..9763477a711 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -346,17 +346,55 @@ describe User do end end - describe '.todo_authors' do - it 'filters users' do - create :user - user_2 = create :user - user_3 = create :user - current_user = create :user - create(:todo, user: current_user, author: user_2, state: :done) - create(:todo, user: current_user, author: user_3, state: :pending) + describe '.limit_to_todo_authors' do + context 'when filtering by todo authors' do + let(:user1) { create(:user) } + let(:user2) { create(:user) } - expect(described_class.todo_authors(current_user.id, 'pending')).to eq [user_3] - expect(described_class.todo_authors(current_user.id, 'done')).to eq [user_2] + before do + create(:todo, user: user1, author: user1, state: :done) + create(:todo, user: user2, author: user2, state: :pending) + end + + it 'only returns users that have authored todos' do + users = described_class.limit_to_todo_authors( + user: user2, + with_todos: true, + todo_state: :pending + ) + + expect(users).to eq([user2]) + end + + it 'ignores users that do not have a todo in the matching state' do + users = described_class.limit_to_todo_authors( + user: user1, + with_todos: true, + todo_state: :pending + ) + + expect(users).to be_empty + end + end + + context 'when not filtering by todo authors' do + it 'returns the input relation' do + user1 = create(:user) + user2 = create(:user) + rel = described_class.limit_to_todo_authors(user: user1) + + expect(rel).to include(user1, user2) + end + end + + context 'when no user is provided' do + it 'returns the input relation' do + user1 = create(:user) + user2 = create(:user) + rel = described_class.limit_to_todo_authors + + expect(rel).to include(user1, user2) + end end end end @@ -2901,4 +2939,86 @@ describe User do let(:uploader_class) { AttachmentUploader } end end + + describe '.union_with_user' do + context 'when no user ID is provided' do + it 'returns the input relation' do + user = create(:user) + + expect(described_class.union_with_user).to eq([user]) + end + end + + context 'when a user ID is provided' do + it 'includes the user object in the returned relation' do + user1 = create(:user) + user2 = create(:user) + users = described_class.where(id: user1.id).union_with_user(user2.id) + + expect(users).to include(user1) + expect(users).to include(user2) + end + + it 'does not re-apply any WHERE conditions on the outer query' do + relation = described_class.where(id: 1).union_with_user(2) + + expect(relation.arel.where_sql).to be_nil + end + end + end + + describe '.optionally_search' do + context 'using nil as the argument' do + it 'returns the current relation' do + user = create(:user) + + expect(described_class.optionally_search).to eq([user]) + end + end + + context 'using an empty String as the argument' do + it 'returns the current relation' do + user = create(:user) + + expect(described_class.optionally_search('')).to eq([user]) + end + end + + context 'using a non-empty String' do + it 'returns users matching the search query' do + user1 = create(:user) + create(:user) + + expect(described_class.optionally_search(user1.name)).to eq([user1]) + end + end + end + + describe '.where_not_in' do + context 'without an argument' do + it 'returns the current relation' do + user = create(:user) + + expect(described_class.where_not_in).to eq([user]) + end + end + + context 'using a list of user IDs' do + it 'excludes the users from the returned relation' do + user1 = create(:user) + user2 = create(:user) + + expect(described_class.where_not_in([user2.id])).to eq([user1]) + end + end + end + + describe '.reorder_by_name' do + it 'reorders the input relation' do + user1 = create(:user, name: 'A') + user2 = create(:user, name: 'B') + + expect(described_class.reorder_by_name).to eq([user1, user2]) + end + end end diff --git a/spec/requests/api/events_spec.rb b/spec/requests/api/events_spec.rb index e6a61fdcf39..573eebe314d 100644 --- a/spec/requests/api/events_spec.rb +++ b/spec/requests/api/events_spec.rb @@ -2,9 +2,9 @@ require 'spec_helper' describe API::Events do include ApiHelpers + let(:user) { create(:user) } let(:non_member) { create(:user) } - let(:other_user) { create(:user, username: 'otheruser') } let(:private_project) { create(:project, :private, creator_id: user.id, namespace: user.namespace) } let(:closed_issue) { create(:closed_issue, project: private_project, author: user) } let!(:closed_issue_event) { create(:event, project: private_project, author: user, target: closed_issue, action: Event::CLOSED, created_at: Date.new(2016, 12, 30)) } @@ -28,12 +28,52 @@ describe API::Events do expect(json_response.size).to eq(1) end end + + context 'when the requesting token has "read_user" scope' do + let(:token) { create(:personal_access_token, scopes: ['read_user'], user: user) } + + it 'returns users events' do + get api('/events?action=closed&target_type=issue&after=2016-12-1&before=2016-12-31', personal_access_token: token) + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.size).to eq(1) + end + end + + context 'when the requesting token does not have "read_user" or "api" scope' do + let(:token_without_scopes) { create(:personal_access_token, scopes: ['read_repository'], user: user) } + + it 'returns a "403" response' do + get api('/events', personal_access_token: token_without_scopes) + + expect(response).to have_gitlab_http_status(403) + end + end end describe 'GET /users/:id/events' do - context "as a user that cannot see the event's project" do - it 'returns no events' do - get api("/users/#{user.id}/events", other_user) + context "as a user that cannot see another user" do + it 'returns a "404" response' do + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?).with(non_member, :read_user, user).and_return(false) + + get api("/users/#{user.id}/events", non_member) + + expect(response).to have_gitlab_http_status(200) + expect(json_response).to be_empty + end + end + + context "as a user token that cannot see another user" do + let(:non_member_token) { create(:personal_access_token, scopes: ['read_user'], user: non_member) } + + it 'returns a "404" response' do + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?).with(non_member, :read_user, user).and_return(false) + + get api("/users/#{user.id}/events", personal_access_token: non_member_token) expect(response).to have_gitlab_http_status(200) expect(json_response).to be_empty diff --git a/spec/serializers/move_to_project_entity_spec.rb b/spec/serializers/move_to_project_entity_spec.rb new file mode 100644 index 00000000000..ac495eadb68 --- /dev/null +++ b/spec/serializers/move_to_project_entity_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe MoveToProjectEntity do + describe '#as_json' do + let(:project) { build(:project, id: 1) } + + subject { described_class.new(project).as_json } + + it 'includes the project ID' do + expect(subject[:id]).to eq(project.id) + end + + it 'includes the full path' do + expect(subject[:name_with_namespace]).to eq(project.name_with_namespace) + end + end +end diff --git a/spec/serializers/move_to_project_serializer_spec.rb b/spec/serializers/move_to_project_serializer_spec.rb new file mode 100644 index 00000000000..841ac969eeb --- /dev/null +++ b/spec/serializers/move_to_project_serializer_spec.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe MoveToProjectSerializer do + describe '#represent' do + it 'includes the name and name with namespace' do + project = build(:project, id: 1) + output = described_class.new.represent(project) + + expect(output).to include(:id, :name_with_namespace) + end + end +end diff --git a/vendor/licenses.csv b/vendor/licenses.csv index a462daf3067..ffe56286684 100644 --- a/vendor/licenses.csv +++ b/vendor/licenses.csv @@ -615,7 +615,6 @@ function-bind,1.1.1,MIT functional-red-black-tree,1.0.1,MIT fuzzaldrin-plus,0.5.0,MIT gauge,2.7.4,ISC -gemnasium-gitlab-service,0.2.6,MIT gemojione,3.3.0,MIT generate-function,2.0.0,MIT generate-object-property,1.2.0,MIT |