diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2019-12-03 18:06:49 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2019-12-03 18:06:49 +0000 |
commit | ab7cf450ba19cf80b9534f25dc707b33845e3014 (patch) | |
tree | bbfa6aba83c48aea68d79c4179ce576b6eec326d | |
parent | 4204cf308596e0e26f578a6e2da88f49c0f4aad9 (diff) | |
download | gitlab-ce-ab7cf450ba19cf80b9534f25dc707b33845e3014.tar.gz |
Add latest changes from gitlab-org/gitlab@master
114 files changed, 1884 insertions, 974 deletions
diff --git a/.eslintrc.yml b/.eslintrc.yml index db03486e9fb..6366feaf897 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -31,7 +31,6 @@ rules: - error - allowElseIf: true import/no-cycle: warn - import/no-unresolved: warn import/no-useless-path-segments: off import/order: warn lines-between-class-members: off diff --git a/app/assets/javascripts/pages/instance_statistics/conversational_development_index/index.js b/app/assets/javascripts/pages/instance_statistics/dev_ops_score/index.js index c1056537f90..c1056537f90 100644 --- a/app/assets/javascripts/pages/instance_statistics/conversational_development_index/index.js +++ b/app/assets/javascripts/pages/instance_statistics/dev_ops_score/index.js diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 25c1d80b117..4d55d7f00f0 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -20,11 +20,11 @@ class ApplicationController < ActionController::Base before_action :authenticate_user!, except: [:route_not_found] before_action :enforce_terms!, if: :should_enforce_terms? before_action :validate_user_service_ticket! - before_action :check_password_expiration + before_action :check_password_expiration, if: :html_request? before_action :ldap_security_check before_action :sentry_context before_action :default_headers - before_action :add_gon_variables, unless: [:peek_request?, :json_request?] + before_action :add_gon_variables, if: :html_request? before_action :configure_permitted_parameters, if: :devise_controller? before_action :require_email, unless: :devise_controller? before_action :active_user_check, unless: :devise_controller? @@ -455,8 +455,8 @@ class ApplicationController < ActionController::Base response.headers['Page-Title'] = URI.escape(page_title('GitLab')) end - def peek_request? - request.path.start_with?('/-/peek') + def html_request? + request.format.html? end def json_request? @@ -466,7 +466,7 @@ class ApplicationController < ActionController::Base def should_enforce_terms? return false unless Gitlab::CurrentSettings.current_application_settings.enforce_terms - !(peek_request? || devise_controller?) + html_request? && !devise_controller? end def set_usage_stats_consent_flag diff --git a/app/controllers/concerns/confirm_email_warning.rb b/app/controllers/concerns/confirm_email_warning.rb index 86df0010665..32e1a46e580 100644 --- a/app/controllers/concerns/confirm_email_warning.rb +++ b/app/controllers/concerns/confirm_email_warning.rb @@ -4,15 +4,18 @@ module ConfirmEmailWarning extend ActiveSupport::Concern included do - before_action :set_confirm_warning, if: -> { Feature.enabled?(:soft_email_confirmation) } + before_action :set_confirm_warning, if: :show_confirm_warning? end protected + def show_confirm_warning? + html_request? && request.get? && Feature.enabled?(:soft_email_confirmation) + end + def set_confirm_warning return unless current_user return if current_user.confirmed? - return if peek_request? || json_request? || !request.get? email = current_user.unconfirmed_email || current_user.email diff --git a/app/controllers/concerns/sourcegraph_gon.rb b/app/controllers/concerns/sourcegraph_gon.rb index ab4abd734fb..01925cf9d4d 100644 --- a/app/controllers/concerns/sourcegraph_gon.rb +++ b/app/controllers/concerns/sourcegraph_gon.rb @@ -4,7 +4,7 @@ module SourcegraphGon extend ActiveSupport::Concern included do - before_action :push_sourcegraph_gon, unless: :json_request? + before_action :push_sourcegraph_gon, if: :html_request? end private diff --git a/app/controllers/concerns/uploads_actions.rb b/app/controllers/concerns/uploads_actions.rb index b87779c22d3..9b3b2c4a482 100644 --- a/app/controllers/concerns/uploads_actions.rb +++ b/app/controllers/concerns/uploads_actions.rb @@ -1,11 +1,16 @@ # frozen_string_literal: true module UploadsActions + extend ActiveSupport::Concern include Gitlab::Utils::StrongMemoize include SendFileUpload UPLOAD_MOUNTS = %w(avatar attachment file logo header_logo favicon).freeze + included do + prepend_before_action :set_request_format_from_path_extension + end + def create uploader = UploadService.new(model, params[:file], uploader_class).execute @@ -64,6 +69,20 @@ module UploadsActions private + # Based on ActionDispatch::Http::MimeNegotiation. We have an + # initializer that monkey-patches this method out (so that repository + # paths don't guess a format based on extension), but we do want this + # behavior when serving uploads. + def set_request_format_from_path_extension + path = request.headers['action_dispatch.original_path'] || request.headers['PATH_INFO'] + + if match = path&.match(/\.(\w+)\z/) + format = Mime[match.captures.first] + + request.format = format.symbol if format + end + end + def uploader_class raise NotImplementedError end diff --git a/app/controllers/instance_statistics/conversational_development_index_controller.rb b/app/controllers/instance_statistics/dev_ops_score_controller.rb index f34347b4d22..238f7fa7707 100644 --- a/app/controllers/instance_statistics/conversational_development_index_controller.rb +++ b/app/controllers/instance_statistics/dev_ops_score_controller.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class InstanceStatistics::ConversationalDevelopmentIndexController < InstanceStatistics::ApplicationController +class InstanceStatistics::DevOpsScoreController < InstanceStatistics::ApplicationController # rubocop: disable CodeReuse/ActiveRecord def index @metric = DevOpsScore::Metric.order(:created_at).last&.present diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index a908da08f57..09754409104 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -46,7 +46,7 @@ class Projects::BranchesController < Projects::ApplicationController def diverging_commit_counts respond_to do |format| format.json do - service = Branches::DivergingCommitCountsService.new(repository) + service = ::Branches::DivergingCommitCountsService.new(repository) branches = BranchesFinder.new(repository, params.permit(names: [])).execute Gitlab::GitalyClient.allow_n_plus_1_calls do @@ -63,7 +63,7 @@ class Projects::BranchesController < Projects::ApplicationController redirect_to_autodeploy = project.empty_repo? && project.deployment_platform.present? - result = CreateBranchService.new(project, current_user) + result = ::Branches::CreateService.new(project, current_user) .execute(branch_name, ref) success = (result[:status] == :success) @@ -102,7 +102,7 @@ class Projects::BranchesController < Projects::ApplicationController def destroy @branch_name = Addressable::URI.unescape(params[:id]) - result = DeleteBranchService.new(project, current_user).execute(@branch_name) + result = ::Branches::DeleteService.new(project, current_user).execute(@branch_name) respond_to do |format| format.html do @@ -118,7 +118,7 @@ class Projects::BranchesController < Projects::ApplicationController end def destroy_all_merged - DeleteMergedBranchesService.new(@project, current_user).async_execute + ::Branches::DeleteMergedService.new(@project, current_user).async_execute redirect_to project_branches_path(@project), notice: _('Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes.') diff --git a/app/finders/clusters/knative_version_role_binding_finder.rb b/app/finders/clusters/knative_version_role_binding_finder.rb new file mode 100644 index 00000000000..06ec5ea557f --- /dev/null +++ b/app/finders/clusters/knative_version_role_binding_finder.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Clusters + class KnativeVersionRoleBindingFinder + attr_reader :cluster + + def initialize(cluster) + @cluster = cluster + end + + def execute + cluster&.kubeclient&.get_cluster_role_bindings&.find do |resource| + resource.metadata.name == Clusters::Kubernetes::GITLAB_KNATIVE_VERSION_ROLE_BINDING_NAME + end + end + end +end diff --git a/app/graphql/mutations/issues/set_confidential.rb b/app/graphql/mutations/issues/set_confidential.rb new file mode 100644 index 00000000000..0fff5518665 --- /dev/null +++ b/app/graphql/mutations/issues/set_confidential.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Mutations + module Issues + class SetConfidential < Base + graphql_name 'IssueSetConfidential' + + argument :confidential, + GraphQL::BOOLEAN_TYPE, + required: true, + description: 'Whether or not to set the issue as a confidential.' + + def resolve(project_path:, iid:, confidential:) + issue = authorized_find!(project_path: project_path, iid: iid) + project = issue.project + + ::Issues::UpdateService.new(project, current_user, confidential: confidential) + .execute(issue) + + { + issue: issue, + errors: issue.errors.full_messages + } + end + end + end +end diff --git a/app/graphql/mutations/todos/mark_done.rb b/app/graphql/mutations/todos/mark_done.rb index 5483708b5c6..d738e387c43 100644 --- a/app/graphql/mutations/todos/mark_done.rb +++ b/app/graphql/mutations/todos/mark_done.rb @@ -16,22 +16,21 @@ module Mutations null: false, description: 'The requested todo' - # rubocop: disable CodeReuse/ActiveRecord def resolve(id:) todo = authorized_find!(id: id) - mark_done(Todo.where(id: todo.id)) unless todo.done? + + mark_done(todo) { todo: todo.reset, errors: errors_on_object(todo) } end - # rubocop: enable CodeReuse/ActiveRecord private def mark_done(todo) - TodoService.new.mark_todos_as_done(todo, current_user) + TodoService.new.mark_todo_as_done(todo, current_user) end end end diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index ecdbba477d7..e8f4ec06177 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -9,6 +9,7 @@ module Types mount_mutation Mutations::AwardEmojis::Add mount_mutation Mutations::AwardEmojis::Remove mount_mutation Mutations::AwardEmojis::Toggle + mount_mutation Mutations::Issues::SetConfidential mount_mutation Mutations::Issues::SetDueDate mount_mutation Mutations::MergeRequests::SetLabels mount_mutation Mutations::MergeRequests::SetLocked diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index caa4478c848..1293b0d0f59 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -425,6 +425,18 @@ module Ci end end + def expanded_kubernetes_namespace + return unless has_environment? + + namespace = options.dig(:environment, :kubernetes, :namespace) + + if namespace.present? + strong_memoize(:expanded_kubernetes_namespace) do + ExpandVariables.expand(namespace, -> { simple_variables }) + end + end + end + def has_environment? environment.present? end diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index 314ef78757d..ae720065387 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -63,7 +63,7 @@ module Clusters default_value_for :authorization_type, :rbac - def predefined_variables(project:, environment_name:) + def predefined_variables(project:, environment_name:, kubernetes_namespace: nil) Gitlab::Ci::Variables::Collection.new.tap do |variables| variables.append(key: 'KUBE_URL', value: api_url) @@ -74,15 +74,15 @@ module Clusters end if !cluster.managed? || cluster.management_project == project - namespace = Gitlab::Kubernetes::DefaultNamespace.new(cluster, project: project).from_environment_name(environment_name) + namespace = kubernetes_namespace || default_namespace(project, environment_name: environment_name) variables .append(key: 'KUBE_TOKEN', value: token, public: false, masked: true) .append(key: 'KUBE_NAMESPACE', value: namespace) .append(key: 'KUBECONFIG', value: kubeconfig(namespace), public: false, file: true) - elsif kubernetes_namespace = find_persisted_namespace(project, environment_name: environment_name) - variables.concat(kubernetes_namespace.predefined_variables) + elsif persisted_namespace = find_persisted_namespace(project, environment_name: environment_name) + variables.concat(persisted_namespace.predefined_variables) end variables.concat(cluster.predefined_variables) @@ -107,6 +107,13 @@ module Clusters private + def default_namespace(project, environment_name:) + Gitlab::Kubernetes::DefaultNamespace.new( + cluster, + project: project + ).from_environment_name(environment_name) + end + def find_persisted_namespace(project, environment_name:) Clusters::KubernetesNamespaceFinder.new( cluster, diff --git a/app/models/concerns/ci/contextable.rb b/app/models/concerns/ci/contextable.rb index b65e9096d4e..5ff537a7837 100644 --- a/app/models/concerns/ci/contextable.rb +++ b/app/models/concerns/ci/contextable.rb @@ -15,7 +15,7 @@ module Ci variables.concat(project.predefined_variables) variables.concat(pipeline.predefined_variables) variables.concat(runner.predefined_variables) if runnable? && runner - variables.concat(project.deployment_variables(environment: environment)) if environment + variables.concat(deployment_variables(environment: environment)) variables.concat(yaml_variables) variables.concat(user_variables) variables.concat(secret_group_variables) @@ -72,6 +72,15 @@ module Ci end end + def deployment_variables(environment:) + return [] unless environment + + project.deployment_variables( + environment: environment, + kubernetes_namespace: expanded_kubernetes_namespace + ) + end + def secret_group_variables return [] unless project.group diff --git a/app/models/dashboard_group_milestone.rb b/app/models/dashboard_group_milestone.rb index cf6094682f3..48c09f4cd6b 100644 --- a/app/models/dashboard_group_milestone.rb +++ b/app/models/dashboard_group_milestone.rb @@ -22,4 +22,8 @@ class DashboardGroupMilestone < GlobalMilestone def dashboard_milestone? true end + + def merge_requests_enabled? + true + end end diff --git a/app/models/dashboard_milestone.rb b/app/models/dashboard_milestone.rb index 9b377b70e5b..fd59b94b737 100644 --- a/app/models/dashboard_milestone.rb +++ b/app/models/dashboard_milestone.rb @@ -12,4 +12,8 @@ class DashboardMilestone < GlobalMilestone def project_milestone? true end + + def merge_requests_enabled? + project.merge_requests_enabled? + end end diff --git a/app/models/group_milestone.rb b/app/models/group_milestone.rb index bfda603c3cb..87338512d99 100644 --- a/app/models/group_milestone.rb +++ b/app/models/group_milestone.rb @@ -41,4 +41,8 @@ class GroupMilestone < GlobalMilestone def legacy_group_milestone? true end + + def merge_requests_enabled? + true + end end diff --git a/app/models/milestone.rb b/app/models/milestone.rb index d0be54eed02..d29eb62af7a 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -274,6 +274,16 @@ class Milestone < ApplicationRecord project_id.present? end + def merge_requests_enabled? + if group_milestone? + # Assume that groups have at least one project with merge requests enabled. + # Otherwise, we would need to load all of the projects from the database. + true + elsif project_milestone? + project&.merge_requests_enabled? + end + end + private # Milestone titles must be unique across project milestones and group milestones diff --git a/app/models/project.rb b/app/models/project.rb index 14207c48f66..03a99577d5c 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1986,12 +1986,16 @@ class Project < ApplicationRecord end end - def deployment_variables(environment:) + def deployment_variables(environment:, kubernetes_namespace: nil) platform = deployment_platform(environment: environment) return [] unless platform.present? - platform.predefined_variables(project: self, environment_name: environment) + platform.predefined_variables( + project: self, + environment_name: environment, + kubernetes_namespace: kubernetes_namespace + ) end def auto_devops_variables diff --git a/app/services/branches/create_service.rb b/app/services/branches/create_service.rb new file mode 100644 index 00000000000..c8afd97e6bf --- /dev/null +++ b/app/services/branches/create_service.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Branches + class CreateService < BaseService + def execute(branch_name, ref, create_master_if_empty: true) + create_master_branch if create_master_if_empty && project.empty_repo? + + result = ::Branches::ValidateNewService.new(project).execute(branch_name) + + return result if result[:status] == :error + + new_branch = repository.add_branch(current_user, branch_name, ref) + + if new_branch + success(new_branch) + else + error("Invalid reference name: #{branch_name}") + end + rescue Gitlab::Git::PreReceiveError => ex + error(ex.message) + end + + def success(branch) + super().merge(branch: branch) + end + + private + + def create_master_branch + project.repository.create_file( + current_user, + '/README.md', + '', + message: 'Add README.md', + branch_name: 'master' + ) + end + end +end diff --git a/app/services/branches/delete_merged_service.rb b/app/services/branches/delete_merged_service.rb new file mode 100644 index 00000000000..9fd5964bf94 --- /dev/null +++ b/app/services/branches/delete_merged_service.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Branches + class DeleteMergedService < BaseService + def async_execute + DeleteMergedBranchesWorker.perform_async(project.id, current_user.id) + end + + def execute + raise Gitlab::Access::AccessDeniedError unless can?(current_user, :push_code, project) + + branches = project.repository.merged_branch_names + # Prevent deletion of branches relevant to open merge requests + branches -= merge_request_branch_names + # Prevent deletion of protected branches + branches = branches.reject { |branch| ProtectedBranch.protected?(project, branch) } + + branches.each do |branch| + ::Branches::DeleteService.new(project, current_user).execute(branch) + end + end + + private + + # rubocop: disable CodeReuse/ActiveRecord + def merge_request_branch_names + # reorder(nil) is necessary for SELECT DISTINCT because default scope adds an ORDER BY + source_names = project.origin_merge_requests.opened.reorder(nil).distinct.pluck(:source_branch) + target_names = project.merge_requests.opened.reorder(nil).distinct.pluck(:target_branch) + (source_names + target_names).uniq + end + # rubocop: enable CodeReuse/ActiveRecord + end +end diff --git a/app/services/branches/delete_service.rb b/app/services/branches/delete_service.rb new file mode 100644 index 00000000000..ca2b4556b58 --- /dev/null +++ b/app/services/branches/delete_service.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Branches + class DeleteService < BaseService + def execute(branch_name) + repository = project.repository + branch = repository.find_branch(branch_name) + + unless current_user.can?(:push_code, project) + return ServiceResponse.error( + message: 'You dont have push access to repo', + http_status: 405) + end + + unless branch + return ServiceResponse.error( + message: 'No such branch', + http_status: 404) + end + + if repository.rm_branch(current_user, branch_name) + ServiceResponse.success(message: 'Branch was deleted') + else + ServiceResponse.error( + message: 'Failed to remove branch', + http_status: 400) + end + rescue Gitlab::Git::PreReceiveError => ex + ServiceResponse.error(message: ex.message, http_status: 400) + end + end +end diff --git a/app/services/branches/validate_new_service.rb b/app/services/branches/validate_new_service.rb new file mode 100644 index 00000000000..e45183d160f --- /dev/null +++ b/app/services/branches/validate_new_service.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Branches + class ValidateNewService < BaseService + def initialize(project) + @project = project + end + + def execute(branch_name, force: false) + return error('Branch name is invalid') unless valid_name?(branch_name) + + if branch_exist?(branch_name) && !force + return error('Branch already exists') + end + + success + rescue Gitlab::Git::PreReceiveError => ex + error(ex.message) + end + + private + + def valid_name?(branch_name) + Gitlab::GitRefValidator.validate(branch_name) + end + + def branch_exist?(branch_name) + project.repository.branch_exists?(branch_name) + end + end +end diff --git a/app/services/clusters/kubernetes/create_or_update_service_account_service.rb b/app/services/clusters/kubernetes/create_or_update_service_account_service.rb index d798dcdcfd3..0fea398d234 100644 --- a/app/services/clusters/kubernetes/create_or_update_service_account_service.rb +++ b/app/services/clusters/kubernetes/create_or_update_service_account_service.rb @@ -49,8 +49,14 @@ module Clusters create_or_update_knative_serving_role create_or_update_knative_serving_role_binding + create_or_update_crossplane_database_role create_or_update_crossplane_database_role_binding + + return unless knative_serving_namespace + + create_or_update_knative_version_role + create_or_update_knative_version_role_binding end private @@ -64,6 +70,12 @@ module Clusters ).ensure_exists! end + def knative_serving_namespace + kubeclient.core_client.get_namespaces.find do |namespace| + namespace.metadata.name == Clusters::Kubernetes::KNATIVE_SERVING_NAMESPACE + end + end + def create_role_or_cluster_role_binding if namespace_creator kubeclient.create_or_update_role_binding(role_binding_resource) @@ -88,6 +100,14 @@ module Clusters kubeclient.update_role_binding(crossplane_database_role_binding_resource) end + def create_or_update_knative_version_role + kubeclient.update_cluster_role(knative_version_role_resource) + end + + def create_or_update_knative_version_role_binding + kubeclient.update_cluster_role_binding(knative_version_role_binding_resource) + end + def service_account_resource Gitlab::Kubernetes::ServiceAccount.new( service_account_name, @@ -166,6 +186,27 @@ module Clusters service_account_name: service_account_name ).generate end + + def knative_version_role_resource + Gitlab::Kubernetes::ClusterRole.new( + name: Clusters::Kubernetes::GITLAB_KNATIVE_VERSION_ROLE_NAME, + rules: [{ + apiGroups: %w(apps), + resources: %w(deployments), + verbs: %w(list get) + }] + ).generate + end + + def knative_version_role_binding_resource + subjects = [{ kind: 'ServiceAccount', name: service_account_name, namespace: service_account_namespace }] + + Gitlab::Kubernetes::ClusterRoleBinding.new( + Clusters::Kubernetes::GITLAB_KNATIVE_VERSION_ROLE_BINDING_NAME, + Clusters::Kubernetes::GITLAB_KNATIVE_VERSION_ROLE_NAME, + subjects + ).generate + end end end end diff --git a/app/services/clusters/kubernetes/kubernetes.rb b/app/services/clusters/kubernetes/kubernetes.rb index d29519999b2..59cb1c4b3a9 100644 --- a/app/services/clusters/kubernetes/kubernetes.rb +++ b/app/services/clusters/kubernetes/kubernetes.rb @@ -12,5 +12,8 @@ module Clusters GITLAB_KNATIVE_SERVING_ROLE_BINDING_NAME = 'gitlab-knative-serving-rolebinding' GITLAB_CROSSPLANE_DATABASE_ROLE_NAME = 'gitlab-crossplane-database-role' GITLAB_CROSSPLANE_DATABASE_ROLE_BINDING_NAME = 'gitlab-crossplane-database-rolebinding' + GITLAB_KNATIVE_VERSION_ROLE_NAME = 'gitlab-knative-version-role' + GITLAB_KNATIVE_VERSION_ROLE_BINDING_NAME = 'gitlab-knative-version-rolebinding' + KNATIVE_SERVING_NAMESPACE = 'knative-serving' end end diff --git a/app/services/commits/commit_patch_service.rb b/app/services/commits/commit_patch_service.rb index 49113c3c691..4fa6c30e901 100644 --- a/app/services/commits/commit_patch_service.rb +++ b/app/services/commits/commit_patch_service.rb @@ -32,7 +32,7 @@ module Commits end def prepare_branch! - branch_result = CreateBranchService.new(project, current_user) + branch_result = ::Branches::CreateService.new(project, current_user) .execute(@branch_name, @start_branch) if branch_result[:status] != :success diff --git a/app/services/commits/create_service.rb b/app/services/commits/create_service.rb index b42494563b2..bd238605ac1 100644 --- a/app/services/commits/create_service.rb +++ b/app/services/commits/create_service.rb @@ -101,7 +101,7 @@ module Commits end def validate_new_branch_name! - result = ValidateNewBranchService.new(project, current_user).execute(@branch_name, force: force?) + result = ::Branches::ValidateNewService.new(project).execute(@branch_name, force: force?) if result[:status] == :error raise_error("Something went wrong when we tried to create '#{@branch_name}' for you: #{result[:message]}") diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb deleted file mode 100644 index d58cb0f9e2b..00000000000 --- a/app/services/create_branch_service.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -class CreateBranchService < BaseService - def execute(branch_name, ref, create_master_if_empty: true) - create_master_branch if create_master_if_empty && project.empty_repo? - - result = ValidateNewBranchService.new(project, current_user) - .execute(branch_name) - - return result if result[:status] == :error - - new_branch = repository.add_branch(current_user, branch_name, ref) - - if new_branch - success(new_branch) - else - error("Invalid reference name: #{branch_name}") - end - rescue Gitlab::Git::PreReceiveError => ex - error(ex.message) - end - - def success(branch) - super().merge(branch: branch) - end - - private - - def create_master_branch - project.repository.create_file( - current_user, - '/README.md', - '', - message: 'Add README.md', - branch_name: 'master' - ) - end -end diff --git a/app/services/delete_branch_service.rb b/app/services/delete_branch_service.rb deleted file mode 100644 index fd41ce54486..00000000000 --- a/app/services/delete_branch_service.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -class DeleteBranchService < BaseService - def execute(branch_name) - repository = project.repository - branch = repository.find_branch(branch_name) - - unless current_user.can?(:push_code, project) - return ServiceResponse.error( - message: 'You dont have push access to repo', - http_status: 405) - end - - unless branch - return ServiceResponse.error( - message: 'No such branch', - http_status: 404) - end - - if repository.rm_branch(current_user, branch_name) - ServiceResponse.success(message: 'Branch was deleted') - else - ServiceResponse.error( - message: 'Failed to remove branch', - http_status: 400) - end - rescue Gitlab::Git::PreReceiveError => ex - ServiceResponse.error(message: ex.message, http_status: 400) - end -end diff --git a/app/services/delete_merged_branches_service.rb b/app/services/delete_merged_branches_service.rb deleted file mode 100644 index 80de897e94b..00000000000 --- a/app/services/delete_merged_branches_service.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -class DeleteMergedBranchesService < BaseService - def async_execute - DeleteMergedBranchesWorker.perform_async(project.id, current_user.id) - end - - def execute - raise Gitlab::Access::AccessDeniedError unless can?(current_user, :push_code, project) - - branches = project.repository.merged_branch_names - # Prevent deletion of branches relevant to open merge requests - branches -= merge_request_branch_names - # Prevent deletion of protected branches - branches = branches.reject { |branch| ProtectedBranch.protected?(project, branch) } - - branches.each do |branch| - DeleteBranchService.new(project, current_user).execute(branch) - end - end - - private - - # rubocop: disable CodeReuse/ActiveRecord - def merge_request_branch_names - # reorder(nil) is necessary for SELECT DISTINCT because default scope adds an ORDER BY - source_names = project.origin_merge_requests.opened.reorder(nil).distinct.pluck(:source_branch) - target_names = project.merge_requests.opened.reorder(nil).distinct.pluck(:target_branch) - (source_names + target_names).uniq - end - # rubocop: enable CodeReuse/ActiveRecord -end diff --git a/app/services/merge_requests/create_from_issue_service.rb b/app/services/merge_requests/create_from_issue_service.rb index 200a34cae04..95fb99d3e7a 100644 --- a/app/services/merge_requests/create_from_issue_service.rb +++ b/app/services/merge_requests/create_from_issue_service.rb @@ -19,7 +19,7 @@ module MergeRequests return error('Not allowed to create merge request') unless can_create_merge_request? return error('Invalid issue iid') unless @issue_iid.present? && issue.present? - result = CreateBranchService.new(target_project, current_user).execute(branch_name, ref) + result = ::Branches::CreateService.new(target_project, current_user).execute(branch_name, ref) return result if result[:status] == :error new_merge_request = create(merge_request) diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index 2eef3eed804..4a109fe4e16 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -99,7 +99,7 @@ module MergeRequests log_info("Post merge finished on JID #{merge_jid} with state #{state}") if delete_source_branch? - DeleteBranchService.new(@merge_request.source_project, branch_deletion_user) + ::Branches::DeleteService.new(@merge_request.source_project, branch_deletion_user) .execute(merge_request.source_branch) end end diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index e7bcca1a38d..55f888d5664 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -179,6 +179,14 @@ class TodoService mark_todos_as_done(todos, current_user) end + def mark_todo_as_done(todo, current_user) + return if todo.done? + + todo.update(state: :done) + + current_user.update_todos_count_cache + end + # When user marks some todos as pending def mark_todos_as_pending(todos, current_user) update_todos_state(todos, current_user, :pending) diff --git a/app/services/validate_new_branch_service.rb b/app/services/validate_new_branch_service.rb deleted file mode 100644 index 3f4a59e5cee..00000000000 --- a/app/services/validate_new_branch_service.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -require_relative 'base_service' - -class ValidateNewBranchService < BaseService - def execute(branch_name, force: false) - valid_branch = Gitlab::GitRefValidator.validate(branch_name) - - unless valid_branch - return error('Branch name is invalid') - end - - if project.repository.branch_exists?(branch_name) && !force - return error('Branch already exists') - end - - success - rescue Gitlab::Git::PreReceiveError => ex - error(ex.message) - end -end diff --git a/app/views/instance_statistics/conversational_development_index/_callout.html.haml b/app/views/instance_statistics/dev_ops_score/_callout.html.haml index 15e31fa3d9c..61f998424f2 100644 --- a/app/views/instance_statistics/conversational_development_index/_callout.html.haml +++ b/app/views/instance_statistics/dev_ops_score/_callout.html.haml @@ -1,5 +1,5 @@ .prepend-top-default -.user-callout{ data: { uid: 'convdev_intro_callout_dismissed' } } +.user-callout{ data: { uid: 'dev_ops_score_intro_callout_dismissed' } } .bordered-box.landing.content-block %button.btn.btn-default.close.js-close-callout{ type: 'button', 'aria-label' => _('Dismiss ConvDev introduction') } @@ -9,5 +9,5 @@ = _('Introducing Your Conversational Development Index') %p = _('Your Conversational Development Index gives an overview of how you are using GitLab from a feature perspective. View how you compare with other organizations, discover features you are not using, and learn best practices through blog posts and white papers.') - .svg-container.devops - = custom_icon('convdev_overview') + .svg-container.convdev + = custom_icon('dev_ops_score_overview') diff --git a/app/views/instance_statistics/conversational_development_index/_card.html.haml b/app/views/instance_statistics/dev_ops_score/_card.html.haml index c63bd96a175..c63bd96a175 100644 --- a/app/views/instance_statistics/conversational_development_index/_card.html.haml +++ b/app/views/instance_statistics/dev_ops_score/_card.html.haml diff --git a/app/views/instance_statistics/conversational_development_index/_disabled.html.haml b/app/views/instance_statistics/dev_ops_score/_disabled.html.haml index ddcbdf6dd27..da27ea17b61 100644 --- a/app/views/instance_statistics/conversational_development_index/_disabled.html.haml +++ b/app/views/instance_statistics/dev_ops_score/_disabled.html.haml @@ -1,6 +1,6 @@ .container.devops-empty .col-sm-12.justify-content-center.text-center - = custom_icon('convdev_no_index') + = custom_icon('dev_ops_score_no_index') %h4= _('Usage ping is not enabled') - if !current_user.admin? %p diff --git a/app/views/instance_statistics/conversational_development_index/_no_data.html.haml b/app/views/instance_statistics/dev_ops_score/_no_data.html.haml index 2031bced4fc..54598244039 100644 --- a/app/views/instance_statistics/conversational_development_index/_no_data.html.haml +++ b/app/views/instance_statistics/dev_ops_score/_no_data.html.haml @@ -1,7 +1,7 @@ .container.devops-empty .col-sm-12.justify-content-center.text-center - = custom_icon('convdev_no_data') + = custom_icon('dev_ops_score_no_data') %h4= _('Data is still calculating...') %p = _('In order to gather accurate feature usage data, it can take 1 to 2 weeks to see your index.') - = link_to _('Learn more'), help_page_path('user/instance_statistics/convdev'), target: '_blank' + = link_to _('Learn more'), help_page_path('user/instance_statistics/dev_ops_score'), target: '_blank' diff --git a/app/views/instance_statistics/conversational_development_index/index.html.haml b/app/views/instance_statistics/dev_ops_score/index.html.haml index f9a40152380..bd457f4740a 100644 --- a/app/views/instance_statistics/conversational_development_index/index.html.haml +++ b/app/views/instance_statistics/dev_ops_score/index.html.haml @@ -2,7 +2,7 @@ - usage_ping_enabled = Gitlab::CurrentSettings.usage_ping_enabled .container - - if usage_ping_enabled && show_callout?('convdev_intro_callout_dismissed') + - if usage_ping_enabled && show_callout?('dev_ops_score_intro_callout_dismissed') = render 'callout' .prepend-top-default @@ -19,7 +19,7 @@ = _('index') %br = _('score') - = link_to icon('question-circle', 'aria-hidden' => 'true'), help_page_path('user/instance_statistics/convdev') + = link_to icon('question-circle', 'aria-hidden' => 'true'), help_page_path('user/instance_statistics/dev_ops_score') .devops-cards.board-card-container - @metric.cards.each do |card| diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index d339751848b..9a839765286 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -48,7 +48,7 @@ %li.dropdown = render_if_exists 'dashboard/nav_link_list' - if can?(current_user, :read_instance_statistics) - = nav_link(controller: [:conversational_development_index, :cohorts]) do + = nav_link(controller: [:dev_ops_score, :cohorts]) do = link_to instance_statistics_root_path do = _('Instance Statistics') - if current_user.admin? diff --git a/app/views/layouts/nav/sidebar/_instance_statistics.html.haml b/app/views/layouts/nav/sidebar/_instance_statistics.html.haml index 57180f27146..6a5f727bb48 100644 --- a/app/views/layouts/nav/sidebar/_instance_statistics.html.haml +++ b/app/views/layouts/nav/sidebar/_instance_statistics.html.haml @@ -6,15 +6,15 @@ = sprite_icon('chart', size: 24) .sidebar-context-title= _('Instance Statistics') %ul.sidebar-top-level-items - = nav_link(controller: :conversational_development_index) do - = link_to instance_statistics_conversational_development_index_index_path do + = nav_link(controller: :dev_ops_score) do + = link_to instance_statistics_dev_ops_score_index_path do .nav-icon-container = sprite_icon('comment') %span.nav-item-name = _('ConvDev Index') %ul.sidebar-sub-level-items.is-fly-out-only - = nav_link(controller: :conversational_development_index, html_options: { class: "fly-out-top-item" } ) do - = link_to instance_statistics_conversational_development_index_index_path do + = nav_link(controller: :dev_ops_score, html_options: { class: "fly-out-top-item" } ) do + = link_to instance_statistics_dev_ops_score_index_path do %strong.fly-out-top-item-name = _('ConvDev Index') diff --git a/app/views/shared/icons/_convdev_no_data.svg b/app/views/shared/icons/_dev_ops_score_no_data.svg index ed32b2333e7..ed32b2333e7 100644 --- a/app/views/shared/icons/_convdev_no_data.svg +++ b/app/views/shared/icons/_dev_ops_score_no_data.svg diff --git a/app/views/shared/icons/_convdev_no_index.svg b/app/views/shared/icons/_dev_ops_score_no_index.svg index 95c00e81d10..95c00e81d10 100644 --- a/app/views/shared/icons/_convdev_no_index.svg +++ b/app/views/shared/icons/_dev_ops_score_no_index.svg diff --git a/app/views/shared/icons/_convdev_overview.svg b/app/views/shared/icons/_dev_ops_score_overview.svg index 2f31113bad7..2f31113bad7 100644 --- a/app/views/shared/icons/_convdev_overview.svg +++ b/app/views/shared/icons/_dev_ops_score_overview.svg diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml index b324f35c338..6e50b31fd71 100644 --- a/app/views/shared/milestones/_milestone.html.haml +++ b/app/views/shared/milestones/_milestone.html.haml @@ -43,8 +43,9 @@ .col-sm-4.milestone-progress = milestone_progress_bar(milestone) = link_to pluralize(milestone.total_issues_count(current_user), 'Issue'), issues_path - · - = link_to pluralize(milestone.merge_requests_visible_to_user(current_user).size, 'Merge Request'), merge_requests_path + - if milestone.merge_requests_enabled? + · + = link_to pluralize(milestone.merge_requests_visible_to_user(current_user).size, 'Merge Request'), merge_requests_path .float-lg-right.light #{milestone.percent_complete(current_user)}% complete .col-sm-2 .milestone-actions.d-flex.justify-content-sm-start.justify-content-md-end diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml index b6656e6283c..fbbcc4f3e68 100644 --- a/app/views/shared/milestones/_sidebar.html.haml +++ b/app/views/shared/milestones/_sidebar.html.haml @@ -105,38 +105,39 @@ = render_if_exists 'shared/milestones/weight', milestone: milestone - .block.merge-requests - .sidebar-collapsed-icon.has-tooltip{ title: milestone_merge_requests_tooltip_text(milestone), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } } - %strong - = custom_icon('mr_bold') - %span= milestone.merge_requests.count - .title.hide-collapsed - Merge requests - %span.badge.badge-pill= milestone.merge_requests.count - .value.hide-collapsed.bold - - if !project || can?(current_user, :read_merge_request, project) - %span.milestone-stat - = link_to milestones_browse_issuables_path(milestone, type: :merge_requests) do + - if milestone.merge_requests_enabled? + .block.merge-requests + .sidebar-collapsed-icon.has-tooltip{ title: milestone_merge_requests_tooltip_text(milestone), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } } + %strong + = custom_icon('mr_bold') + %span= milestone.merge_requests.count + .title.hide-collapsed + Merge requests + %span.badge.badge-pill= milestone.merge_requests.count + .value.hide-collapsed.bold + - if !project || can?(current_user, :read_merge_request, project) + %span.milestone-stat + = link_to milestones_browse_issuables_path(milestone, type: :merge_requests) do + Open: + = milestone.merge_requests.opened.count + %span.milestone-stat + = link_to milestones_browse_issuables_path(milestone, type: :merge_requests, state: 'closed') do + Closed: + = milestone.merge_requests.closed.count + %span.milestone-stat + = link_to milestones_browse_issuables_path(milestone, type: :merge_requests, state: 'merged') do + Merged: + = milestone.merge_requests.merged.count + - else + %span.milestone-stat Open: = milestone.merge_requests.opened.count - %span.milestone-stat - = link_to milestones_browse_issuables_path(milestone, type: :merge_requests, state: 'closed') do + %span.milestone-stat Closed: = milestone.merge_requests.closed.count - %span.milestone-stat - = link_to milestones_browse_issuables_path(milestone, type: :merge_requests, state: 'merged') do + %span.milestone-stat Merged: = milestone.merge_requests.merged.count - - else - %span.milestone-stat - Open: - = milestone.merge_requests.opened.count - %span.milestone-stat - Closed: - = milestone.merge_requests.closed.count - %span.milestone-stat - Merged: - = milestone.merge_requests.merged.count - if project - recent_releases, total_count, more_count = recent_releases_with_counts(milestone) diff --git a/app/views/shared/milestones/_tabs.html.haml b/app/views/shared/milestones/_tabs.html.haml index f718c5767d1..538ebe79641 100644 --- a/app/views/shared/milestones/_tabs.html.haml +++ b/app/views/shared/milestones/_tabs.html.haml @@ -6,10 +6,11 @@ = link_to '#tab-issues', class: 'nav-link active', data: { toggle: 'tab', show: '.tab-issues-buttons' } do = _('Issues') %span.badge.badge-pill= milestone.issues_visible_to_user(current_user).size - %li.nav-item - = link_to '#tab-merge-requests', class: 'nav-link', data: { toggle: 'tab', endpoint: milestone_tab_path(milestone, 'merge_requests') } do - = _('Merge Requests') - %span.badge.badge-pill= milestone.merge_requests_visible_to_user(current_user).size + - if milestone.merge_requests_enabled? + %li.nav-item + = link_to '#tab-merge-requests', class: 'nav-link', data: { toggle: 'tab', endpoint: milestone_tab_path(milestone, 'merge_requests') } do + = _('Merge Requests') + %span.badge.badge-pill= milestone.merge_requests_visible_to_user(current_user).size %li.nav-item = link_to '#tab-participants', class: 'nav-link', data: { toggle: 'tab', endpoint: milestone_tab_path(milestone, 'participants') } do = _('Participants') @@ -26,9 +27,10 @@ .tab-content.milestone-content .tab-pane.active#tab-issues{ data: { sort_endpoint: (sort_issues_project_milestone_path(@project, @milestone) if @project && current_user) } } = render 'shared/milestones/issues_tab', issues: issues, show_project_name: show_project_name, show_full_project_name: show_full_project_name - .tab-pane#tab-merge-requests - -# loaded async - = render "shared/milestones/tab_loading" + - if milestone.merge_requests_enabled? + .tab-pane#tab-merge-requests + -# loaded async + = render "shared/milestones/tab_loading" .tab-pane#tab-participants -# loaded async = render "shared/milestones/tab_loading" diff --git a/app/workers/delete_merged_branches_worker.rb b/app/workers/delete_merged_branches_worker.rb index 44b3db30d0d..f3d86233c1b 100644 --- a/app/workers/delete_merged_branches_worker.rb +++ b/app/workers/delete_merged_branches_worker.rb @@ -15,7 +15,7 @@ class DeleteMergedBranchesWorker user = User.find(user_id) begin - DeleteMergedBranchesService.new(project, user).execute + ::Branches::DeleteMergedService.new(project, user).execute rescue Gitlab::Access::AccessDeniedError return end diff --git a/changelogs/unreleased/27630-deploy-to-ci-specified-namespace.yml b/changelogs/unreleased/27630-deploy-to-ci-specified-namespace.yml new file mode 100644 index 00000000000..f743f780837 --- /dev/null +++ b/changelogs/unreleased/27630-deploy-to-ci-specified-namespace.yml @@ -0,0 +1,5 @@ +--- +title: Use CI configured namespace for deployments to unmanaged clusters +merge_request: 20686 +author: +type: added diff --git a/changelogs/unreleased/36318-graphql-mutation-for-changing-confidential-status-of-an-issue.yml b/changelogs/unreleased/36318-graphql-mutation-for-changing-confidential-status-of-an-issue.yml new file mode 100644 index 00000000000..5c54495d2ec --- /dev/null +++ b/changelogs/unreleased/36318-graphql-mutation-for-changing-confidential-status-of-an-issue.yml @@ -0,0 +1,5 @@ +--- +title: Add GraphQL mutation for setting an issue as confidential +merge_request: 20785 +author: +type: added diff --git a/changelogs/unreleased/36717-container-repositories-can-not-be-replicated.yml b/changelogs/unreleased/36717-container-repositories-can-not-be-replicated.yml new file mode 100644 index 00000000000..6a27ddeb0cd --- /dev/null +++ b/changelogs/unreleased/36717-container-repositories-can-not-be-replicated.yml @@ -0,0 +1,5 @@ +--- +title: Fix Container repositories can not be replicated when s3 is used +merge_request: 21068 +author: +type: fixed diff --git a/changelogs/unreleased/37385-respect-commit-timezones-from-gitaly.yml b/changelogs/unreleased/37385-respect-commit-timezones-from-gitaly.yml new file mode 100644 index 00000000000..3e8302ce845 --- /dev/null +++ b/changelogs/unreleased/37385-respect-commit-timezones-from-gitaly.yml @@ -0,0 +1,5 @@ +--- +title: Respect the timezone reported from Gitaly +merge_request: 21066 +author: +type: fixed diff --git a/changelogs/unreleased/gitlabktl-17-add-rbac-permissions-for-knative-version.yml b/changelogs/unreleased/gitlabktl-17-add-rbac-permissions-for-knative-version.yml new file mode 100644 index 00000000000..5baf993629e --- /dev/null +++ b/changelogs/unreleased/gitlabktl-17-add-rbac-permissions-for-knative-version.yml @@ -0,0 +1,5 @@ +--- +title: Add rbac access to knative-serving namespace deployments to get knative version information +merge_request: 20244 +author: +type: changed diff --git a/changelogs/unreleased/wolf-feat-milestone-hide-mr.yml b/changelogs/unreleased/wolf-feat-milestone-hide-mr.yml new file mode 100644 index 00000000000..cae6f6a8894 --- /dev/null +++ b/changelogs/unreleased/wolf-feat-milestone-hide-mr.yml @@ -0,0 +1,5 @@ +--- +title: Hide Merge Request information on milestones when MRs are disabled for project +merge_request: 20985 +author: Wolfgang Faust +type: changed diff --git a/config/routes/instance_statistics.rb b/config/routes/instance_statistics.rb index 1102ef6b017..967255d5b82 100644 --- a/config/routes/instance_statistics.rb +++ b/config/routes/instance_statistics.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true namespace :instance_statistics do - root to: redirect('-/instance_statistics/conversational_development_index') + root to: redirect('-/instance_statistics/dev_ops_score') resources :cohorts, only: :index - resources :conversational_development_index, only: :index + resources :dev_ops_score, only: :index end diff --git a/config/webpack.config.js b/config/webpack.config.js index 9c7a3f42c97..f69a799ebcf 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -102,6 +102,7 @@ const alias = { if (IS_EE) { Object.assign(alias, { ee: path.join(ROOT_PATH, 'ee/app/assets/javascripts'), + ee_component: path.join(ROOT_PATH, 'ee/app/assets/javascripts'), ee_empty_states: path.join(ROOT_PATH, 'ee/app/views/shared/empty_states'), ee_icons: path.join(ROOT_PATH, 'ee/app/views/shared/icons'), ee_images: path.join(ROOT_PATH, 'ee/app/assets/images'), @@ -283,16 +284,13 @@ module.exports = { jQuery: 'jquery', }), - new webpack.NormalModuleReplacementPlugin(/^ee_component\/(.*)\.vue/, function(resource) { - if (Object.keys(module.exports.resolve.alias).indexOf('ee') >= 0) { - resource.request = resource.request.replace(/^ee_component/, 'ee'); - } else { + !IS_EE && + new webpack.NormalModuleReplacementPlugin(/^ee_component\/(.*)\.vue/, resource => { resource.request = path.join( ROOT_PATH, 'app/assets/javascripts/vue_shared/components/empty_component.js', ); - } - }), + }), new CopyWebpackPlugin([ { diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index d8902be92eb..fc3c16abdaa 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -2519,6 +2519,51 @@ type IssuePermissions { } """ +Autogenerated input type of IssueSetConfidential +""" +input IssueSetConfidentialInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Whether or not to set the issue as a confidential. + """ + confidential: Boolean! + + """ + The iid of the issue to mutate + """ + iid: String! + + """ + The project the issue to mutate is in + """ + projectPath: ID! +} + +""" +Autogenerated return type of IssueSetConfidential +""" +type IssueSetConfidentialPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Reasons why the mutation failed. + """ + errors: [String!]! + + """ + The issue after mutation + """ + issue: Issue +} + +""" Autogenerated input type of IssueSetDueDate """ input IssueSetDueDateInput { @@ -3556,6 +3601,7 @@ type Mutation { destroyNote(input: DestroyNoteInput!): DestroyNotePayload epicSetSubscription(input: EpicSetSubscriptionInput!): EpicSetSubscriptionPayload epicTreeReorder(input: EpicTreeReorderInput!): EpicTreeReorderPayload + issueSetConfidential(input: IssueSetConfidentialInput!): IssueSetConfidentialPayload issueSetDueDate(input: IssueSetDueDateInput!): IssueSetDueDatePayload mergeRequestSetAssignees(input: MergeRequestSetAssigneesInput!): MergeRequestSetAssigneesPayload mergeRequestSetLabels(input: MergeRequestSetLabelsInput!): MergeRequestSetLabelsPayload diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index fba3dcca14d..09f91e0f02f 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -14113,6 +14113,33 @@ "deprecationReason": null }, { + "name": "issueSetConfidential", + "description": null, + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "IssueSetConfidentialInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "IssueSetConfidentialPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "issueSetDueDate", "description": null, "args": [ @@ -14987,6 +15014,136 @@ }, { "kind": "OBJECT", + "name": "IssueSetConfidentialPayload", + "description": "Autogenerated return type of IssueSetConfidential", + "fields": [ + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": "Reasons why the mutation failed.", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "issue", + "description": "The issue after mutation", + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "Issue", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "IssueSetConfidentialInput", + "description": "Autogenerated input type of IssueSetConfidential", + "fields": null, + "inputFields": [ + { + "name": "projectPath", + "description": "The project the issue to mutate is in", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "iid", + "description": "The iid of the issue to mutate", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "confidential", + "description": "Whether or not to set the issue as a confidential.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", "name": "IssueSetDueDatePayload", "description": "Autogenerated return type of IssueSetDueDate", "fields": [ diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 4b71c9a8eaf..772648ec715 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -375,6 +375,14 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph | `createDesign` | Boolean! | Whether or not a user can perform `create_design` on this resource | | `destroyDesign` | Boolean! | Whether or not a user can perform `destroy_design` on this resource | +### IssueSetConfidentialPayload + +| Name | Type | Description | +| --- | ---- | ---------- | +| `clientMutationId` | String | A unique identifier for the client performing the mutation. | +| `errors` | String! => Array | Reasons why the mutation failed. | +| `issue` | Issue | The issue after mutation | + ### IssueSetDueDatePayload | Name | Type | Description | diff --git a/doc/development/experiment_guide/index.md b/doc/development/experiment_guide/index.md index 5155433c9ad..c7a80a07c61 100644 --- a/doc/development/experiment_guide/index.md +++ b/doc/development/experiment_guide/index.md @@ -55,7 +55,7 @@ The author then adds a comment to this piece of code and adds a link to the issu end ``` -- [ ] Track necessery events. See the [event tracking guide](../event_tracking/index.md) for details. +- [ ] Track necessary events. See the [event tracking guide](../event_tracking/index.md) for details. - [ ] After the merge request is merged, use [`chatops`](../../ci/chatops/README.md) to enable the feature flag and start the experiment. For visibility, please run the command in the `#s_growth` channel: ``` diff --git a/doc/development/testing_guide/frontend_testing.md b/doc/development/testing_guide/frontend_testing.md index 236f175cee5..6a2cc5eeb6b 100644 --- a/doc/development/testing_guide/frontend_testing.md +++ b/doc/development/testing_guide/frontend_testing.md @@ -26,7 +26,7 @@ If you need to update an existing Karma test file (found in `spec/javascripts`), need to migrate the whole spec to Jest. Simply updating the Karma spec to test your change is fine. It is probably more appropriate to migrate to Jest in a separate merge request. -If you need to create a new test file, we strongly recommend creating one in Jest. This will +If you create a new test file, it needs to be created in Jest. This will help support our migration and we think you'll love using Jest. As always, please use discretion. Jest solves a lot of issues we experienced in Karma and diff --git a/doc/user/admin_area/license.md b/doc/user/admin_area/license.md index dbcf250bc57..fe8903a9f01 100644 --- a/doc/user/admin_area/license.md +++ b/doc/user/admin_area/license.md @@ -67,7 +67,7 @@ Omnibus installations should add this entry to `gitlab.rb`: gitlab_rails['license_file'] = "/path/to/license/file" ``` -CAUTION:: **Caution:** +CAUTION: **Caution:** These methods will only add a license at the time of installation. Use the admin area in the web ui to renew or upgrade licenses. diff --git a/doc/user/project/clusters/serverless/index.md b/doc/user/project/clusters/serverless/index.md index ffd7b0c0f2a..21a5d41bf86 100644 --- a/doc/user/project/clusters/serverless/index.md +++ b/doc/user/project/clusters/serverless/index.md @@ -116,7 +116,8 @@ You must do the following: 1. Ensure GitLab can manage Knative: - For a non-GitLab managed cluster, ensure that the service account for the token - provided can manage resources in the `serving.knative.dev` API group. + provided can manage resources in the `serving.knative.dev` API group. It will also + need list access to the deployments in the `knative-serving` namespace. - For a GitLab managed cluster, if you added the cluster in [GitLab 12.1 or later](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/30235), then GitLab will already have the required access and you can proceed to the next step. @@ -153,6 +154,19 @@ You must do the following: - delete - patch - watch + --- + apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRole + metadata: + name: gitlab-knative-version-role + rules: + - apiGroups: + - apps + resources: + - deployments + verbs: + - list + - get ``` Then run the following command: diff --git a/lib/api/branches.rb b/lib/api/branches.rb index 054242dca4c..ce3ee0d7e61 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -137,7 +137,7 @@ module API post ':id/repository/branches' do authorize_push_project - result = CreateBranchService.new(user_project, current_user) + result = ::Branches::CreateService.new(user_project, current_user) .execute(params[:branch], params[:ref]) if result[:status] == :success @@ -162,7 +162,7 @@ module API commit = user_project.repository.commit(branch.dereferenced_target) destroy_conditionally!(commit, last_updated: commit.authored_date) do - result = DeleteBranchService.new(user_project, current_user) + result = ::Branches::DeleteService.new(user_project, current_user) .execute(params[:branch]) if result.error? @@ -173,7 +173,7 @@ module API desc 'Delete all merged branches' delete ':id/repository/merged_branches' do - DeleteMergedBranchesService.new(user_project, current_user).async_execute + ::Branches::DeleteMergedService.new(user_project, current_user).async_execute accepted! end diff --git a/lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb b/lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb index 9950e1dec55..b47238a3083 100644 --- a/lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb +++ b/lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb @@ -8,7 +8,7 @@ module Gitlab def unmet? deployment_cluster.present? && deployment_cluster.managed? && - missing_namespace? + (missing_namespace? || missing_knative_version_role_binding?) end def complete! @@ -23,6 +23,10 @@ module Gitlab kubernetes_namespace.nil? || kubernetes_namespace.service_account_token.blank? end + def missing_knative_version_role_binding? + knative_version_role_binding.nil? + end + def deployment_cluster build.deployment&.cluster end @@ -31,6 +35,14 @@ module Gitlab build.deployment.environment end + def knative_version_role_binding + strong_memoize(:knative_version_role_binding) do + Clusters::KnativeVersionRoleBindingFinder.new( + deployment_cluster + ).execute + end + end + def kubernetes_namespace strong_memoize(:kubernetes_namespace) do Clusters::KubernetesNamespaceFinder.new( diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb index 6210223917b..b2dc9a8a3c8 100644 --- a/lib/gitlab/git/commit.rb +++ b/lib/gitlab/git/commit.rb @@ -370,15 +370,26 @@ module Gitlab # subject from the message to make it clearer when there's one # available but not the other. @message = message_from_gitaly_body - @authored_date = Time.at(commit.author.date.seconds).utc + @authored_date = init_date_from_gitaly(commit.author) @author_name = commit.author.name.dup @author_email = commit.author.email.dup - @committed_date = Time.at(commit.committer.date.seconds).utc + + @committed_date = init_date_from_gitaly(commit.committer) @committer_name = commit.committer.name.dup @committer_email = commit.committer.email.dup @parent_ids = Array(commit.parent_ids) end + # Gitaly provides a UNIX timestamp in author.date.seconds, and a timezone + # offset in author.timezone. If the latter isn't present, assume UTC. + def init_date_from_gitaly(author) + if author.timezone.present? + Time.strptime("#{author.date.seconds} #{author.timezone}", '%s %z') + else + Time.at(author.date.seconds).utc + end + end + def serialize_keys SERIALIZE_KEYS end diff --git a/lib/gitlab/kubernetes/cluster_role.rb b/lib/gitlab/kubernetes/cluster_role.rb new file mode 100644 index 00000000000..4d40736a0b5 --- /dev/null +++ b/lib/gitlab/kubernetes/cluster_role.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Gitlab + module Kubernetes + class ClusterRole + attr_reader :name, :rules + + def initialize(name:, rules:) + @name = name + @rules = rules + end + + def generate + ::Kubeclient::Resource.new( + metadata: metadata, + rules: rules + ) + end + + private + + def metadata + { + name: name + } + end + end + end +end diff --git a/lib/gitlab/kubernetes/kube_client.rb b/lib/gitlab/kubernetes/kube_client.rb index 66c28a9b702..b23ca095414 100644 --- a/lib/gitlab/kubernetes/kube_client.rb +++ b/lib/gitlab/kubernetes/kube_client.rb @@ -56,6 +56,7 @@ module Gitlab # group client delegate :create_cluster_role_binding, :get_cluster_role_binding, + :get_cluster_role_bindings, :update_cluster_role_binding, to: :rbac_client @@ -68,6 +69,13 @@ module Gitlab # RBAC methods delegates to the apis/rbac.authorization.k8s.io api # group client + delegate :create_cluster_role, + :get_cluster_role, + :update_cluster_role, + to: :rbac_client + + # RBAC methods delegates to the apis/rbac.authorization.k8s.io api + # group client delegate :create_role_binding, :get_role_binding, :update_role_binding, diff --git a/rubocop/cop/avoid_route_redirect_leading_slash.rb b/rubocop/cop/avoid_route_redirect_leading_slash.rb index 261d151fb1b..d66e434dc9c 100644 --- a/rubocop/cop/avoid_route_redirect_leading_slash.rb +++ b/rubocop/cop/avoid_route_redirect_leading_slash.rb @@ -7,10 +7,10 @@ module RuboCop # # @example # # bad - # root to: redirect('/-/instance/statistics/conversational_development_index') + # root to: redirect('/-/instance/statistics/dev_ops_score') # # # good - # root to: redirect('-/instance/statistics/conversational_development_index') + # root to: redirect('-/instance/statistics/dev_ops_score') # class AvoidRouteRedirectLeadingSlash < RuboCop::Cop::Cop diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index 4a10e7b5325..04bbffc587f 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -90,14 +90,6 @@ describe ApplicationController do let(:format) { :html } it_behaves_like 'setting gon variables' - - context 'for peek requests' do - before do - request.path = '/-/peek' - end - - it_behaves_like 'not setting gon variables' - end end context 'with json format' do @@ -105,6 +97,12 @@ describe ApplicationController do it_behaves_like 'not setting gon variables' end + + context 'with atom format' do + let(:format) { :atom } + + it_behaves_like 'not setting gon variables' + end end describe 'session expiration' do diff --git a/spec/controllers/instance_statistics/conversational_development_index_controller_spec.rb b/spec/controllers/instance_statistics/dev_ops_score_controller_spec.rb index 4935cb265bf..5825c6295f6 100644 --- a/spec/controllers/instance_statistics/conversational_development_index_controller_spec.rb +++ b/spec/controllers/instance_statistics/dev_ops_score_controller_spec.rb @@ -2,6 +2,6 @@ require 'spec_helper' -describe InstanceStatistics::ConversationalDevelopmentIndexController do +describe InstanceStatistics::DevOpsScoreController do it_behaves_like 'instance statistics availability' end diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb index affe0e0f970..4f8ab6a5def 100644 --- a/spec/controllers/projects/branches_controller_spec.rb +++ b/spec/controllers/projects/branches_controller_spec.rb @@ -178,7 +178,7 @@ describe Projects::BranchesController do it 'redirects to newly created branch' do result = { status: :success, branch: double(name: branch) } - expect_any_instance_of(CreateBranchService).to receive(:execute).and_return(result) + expect_any_instance_of(::Branches::CreateService).to receive(:execute).and_return(result) expect(SystemNoteService).to receive(:new_issue_branch).and_return(true) post :create, @@ -200,7 +200,7 @@ describe Projects::BranchesController do it 'redirects to autodeploy setup page' do result = { status: :success, branch: double(name: branch) } - expect_any_instance_of(CreateBranchService).to receive(:execute).and_return(result) + expect_any_instance_of(::Branches::CreateService).to receive(:execute).and_return(result) expect(SystemNoteService).to receive(:new_issue_branch).and_return(true) post :create, @@ -221,7 +221,7 @@ describe Projects::BranchesController do create(:cluster, :provided_by_gcp, projects: [project]) - expect_any_instance_of(CreateBranchService).to receive(:execute).and_return(result) + expect_any_instance_of(::Branches::CreateService).to receive(:execute).and_return(result) expect(SystemNoteService).to receive(:new_issue_branch).and_return(true) post :create, @@ -459,7 +459,7 @@ describe Projects::BranchesController do end it 'starts worker to delete merged branches' do - expect_any_instance_of(DeleteMergedBranchesService).to receive(:async_execute) + expect_any_instance_of(::Branches::DeleteMergedService).to receive(:async_execute) destroy_all_merged end diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb index 1bcf3bb106b..f35babc1b56 100644 --- a/spec/controllers/uploads_controller_spec.rb +++ b/spec/controllers/uploads_controller_spec.rb @@ -228,10 +228,10 @@ describe UploadsController do user.block end - it "redirects to the sign in page" do + it "responds with status 401" do get :show, params: { model: "user", mounted_as: "avatar", id: user.id, filename: "dk.png" } - expect(response).to redirect_to(new_user_session_path) + expect(response).to have_gitlab_http_status(401) end end @@ -320,10 +320,10 @@ describe UploadsController do end context "when not signed in" do - it "redirects to the sign in page" do + it "responds with status 401" do get :show, params: { model: "project", mounted_as: "avatar", id: project.id, filename: "dk.png" } - expect(response).to redirect_to(new_user_session_path) + expect(response).to have_gitlab_http_status(401) end end @@ -343,10 +343,10 @@ describe UploadsController do project.add_maintainer(user) end - it "redirects to the sign in page" do + it "responds with status 401" do get :show, params: { model: "project", mounted_as: "avatar", id: project.id, filename: "dk.png" } - expect(response).to redirect_to(new_user_session_path) + expect(response).to have_gitlab_http_status(401) end end @@ -439,10 +439,10 @@ describe UploadsController do user.block end - it "redirects to the sign in page" do + it "responds with status 401" do get :show, params: { model: "group", mounted_as: "avatar", id: group.id, filename: "dk.png" } - expect(response).to redirect_to(new_user_session_path) + expect(response).to have_gitlab_http_status(401) end end @@ -526,10 +526,10 @@ describe UploadsController do end context "when not signed in" do - it "redirects to the sign in page" do + it "responds with status 401" do get :show, params: { model: "note", mounted_as: "attachment", id: note.id, filename: "dk.png" } - expect(response).to redirect_to(new_user_session_path) + expect(response).to have_gitlab_http_status(401) end end @@ -549,10 +549,10 @@ describe UploadsController do project.add_maintainer(user) end - it "redirects to the sign in page" do + it "responds with status 401" do get :show, params: { model: "note", mounted_as: "attachment", id: note.id, filename: "dk.png" } - expect(response).to redirect_to(new_user_session_path) + expect(response).to have_gitlab_http_status(401) end end diff --git a/spec/features/dashboard/milestones_spec.rb b/spec/features/dashboard/milestones_spec.rb index c21bc922de7..4ad19710d90 100644 --- a/spec/features/dashboard/milestones_spec.rb +++ b/spec/features/dashboard/milestones_spec.rb @@ -30,6 +30,7 @@ describe 'Dashboard > Milestones' do expect(current_path).to eq dashboard_milestones_path expect(page).to have_content(milestone.title) expect(page).to have_content(group.name) + expect(first('.milestone')).to have_content('Merge Requests') end describe 'new milestones dropdown', :js do @@ -46,4 +47,23 @@ describe 'Dashboard > Milestones' do end end end + + describe 'with merge requests disabled' do + let(:user) { create(:user) } + let(:group) { create(:group) } + let(:project) { create(:project, :merge_requests_disabled, namespace: user.namespace) } + let!(:milestone) { create(:milestone, project: project) } + + before do + group.add_developer(user) + sign_in(user) + visit dashboard_milestones_path + end + + it 'does not see milestones' do + expect(current_path).to eq dashboard_milestones_path + expect(page).to have_content(milestone.title) + expect(first('.milestone')).to have_no_content('Merge Requests') + end + end end diff --git a/spec/features/instance_statistics/conversational_development_index_spec.rb b/spec/features/instance_statistics/dev_ops_score_spec.rb index 6d05682fcd5..c9e6ab67267 100644 --- a/spec/features/instance_statistics/conversational_development_index_spec.rb +++ b/spec/features/instance_statistics/dev_ops_score_spec.rb @@ -2,13 +2,13 @@ require 'spec_helper' -describe 'Conversational Development Index' do +describe 'Dev Ops Score' do before do sign_in(create(:admin)) end it 'has dismissable intro callout', :js do - visit instance_statistics_conversational_development_index_index_path + visit instance_statistics_dev_ops_score_index_path expect(page).to have_content 'Introducing Your Conversational Development Index' @@ -23,13 +23,13 @@ describe 'Conversational Development Index' do end it 'shows empty state' do - visit instance_statistics_conversational_development_index_index_path + visit instance_statistics_dev_ops_score_index_path expect(page).to have_content('Usage ping is not enabled') end it 'hides the intro callout' do - visit instance_statistics_conversational_development_index_index_path + visit instance_statistics_dev_ops_score_index_path expect(page).not_to have_content 'Introducing Your Conversational Development Index' end @@ -39,7 +39,7 @@ describe 'Conversational Development Index' do it 'shows empty state' do stub_application_setting(usage_ping_enabled: true) - visit instance_statistics_conversational_development_index_index_path + visit instance_statistics_dev_ops_score_index_path expect(page).to have_content('Data is still calculating') end @@ -50,7 +50,7 @@ describe 'Conversational Development Index' do stub_application_setting(usage_ping_enabled: true) create(:dev_ops_score_metric) - visit instance_statistics_conversational_development_index_index_path + visit instance_statistics_dev_ops_score_index_path expect(page).to have_content( 'Issues created per active user 1.2 You 9.3 Lead 13.3%' diff --git a/spec/features/merge_request/user_sees_deleted_target_branch_spec.rb b/spec/features/merge_request/user_sees_deleted_target_branch_spec.rb index 224261dec00..9ef6847f7f5 100644 --- a/spec/features/merge_request/user_sees_deleted_target_branch_spec.rb +++ b/spec/features/merge_request/user_sees_deleted_target_branch_spec.rb @@ -9,7 +9,7 @@ describe 'Merge request > User sees deleted target branch', :js do before do project.add_maintainer(user) - DeleteBranchService.new(project, user).execute('feature') + ::Branches::DeleteService.new(project, user).execute('feature') sign_in(user) visit project_merge_request_path(project, merge_request) end diff --git a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb index c42eb8560a4..22b2ea81b32 100644 --- a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb +++ b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb @@ -178,10 +178,11 @@ describe 'Merge request > User selects branches for new MR', :js do end context 'with special characters in branch names' do + let(:create_branch_service) { ::Branches::CreateService.new(project, user) } + it 'escapes quotes in branch names' do special_branch_name = '"with-quotes"' - CreateBranchService.new(project, user) - .execute(special_branch_name, 'add-pdf-file') + create_branch_service.execute(special_branch_name, 'add-pdf-file') visit project_new_merge_request_path(project) select_source_branch(special_branch_name) @@ -192,8 +193,7 @@ describe 'Merge request > User selects branches for new MR', :js do it 'does not escape unicode in branch names' do special_branch_name = 'ʕ•ᴥ•ʔ' - CreateBranchService.new(project, user) - .execute(special_branch_name, 'add-pdf-file') + create_branch_service.execute(special_branch_name, 'add-pdf-file') visit project_new_merge_request_path(project) select_source_branch(special_branch_name) diff --git a/spec/features/milestones/user_views_milestones_spec.rb b/spec/features/milestones/user_views_milestones_spec.rb index 09378cab5e3..c91fe95aa77 100644 --- a/spec/features/milestones/user_views_milestones_spec.rb +++ b/spec/features/milestones/user_views_milestones_spec.rb @@ -18,6 +18,7 @@ describe "User views milestones" do expect(page).to have_content(milestone.title) .and have_content(milestone.expires_at) .and have_content("Issues") + .and have_content("Merge Requests") end context "with issues" do @@ -32,6 +33,7 @@ describe "User views milestones" do .and have_selector("#tab-issues li.issuable-row", count: 2) .and have_content(issue.title) .and have_content(closed_issue.title) + .and have_selector("#tab-merge-requests") end end @@ -62,3 +64,32 @@ describe "User views milestones" do end end end + +describe "User views milestones with no MR" do + set(:user) { create(:user) } + set(:project) { create(:project, :merge_requests_disabled) } + set(:milestone) { create(:milestone, project: project) } + + before do + project.add_developer(user) + sign_in(user) + + visit(project_milestones_path(project)) + end + + it "shows milestone" do + expect(page).to have_content(milestone.title) + .and have_content(milestone.expires_at) + .and have_content("Issues") + .and have_no_content("Merge Requests") + end + + it "opens milestone" do + click_link(milestone.title) + + expect(current_path).to eq(project_milestone_path(project, milestone)) + expect(page).to have_content(milestone.title) + .and have_selector("#tab-issues") + .and have_no_selector("#tab-merge-requests") + end +end diff --git a/spec/frontend/environment.js b/spec/frontend/environment.js index 3c6553f3547..cd4fae60049 100644 --- a/spec/frontend/environment.js +++ b/spec/frontend/environment.js @@ -31,6 +31,7 @@ class CustomEnvironment extends JSDOMEnvironment { this.global.gon = { ee: IS_EE, }; + this.global.IS_EE = IS_EE; this.rejectedPromises = []; diff --git a/spec/frontend/monitoring/mock_data.js b/spec/frontend/monitoring/mock_data.js index 758e86235be..a184892773b 100644 --- a/spec/frontend/monitoring/mock_data.js +++ b/spec/frontend/monitoring/mock_data.js @@ -1,5 +1,10 @@ +// This import path needs to be relative for now because this mock data is used in +// Karma specs too, where the helpers/test_constants alias can not be resolved +import { TEST_HOST } from '../helpers/test_constants'; + export const mockHost = 'http://test.host'; export const mockProjectDir = '/frontend-fixtures/environments-project'; +export const mockApiEndpoint = `${TEST_HOST}/monitoring/mock`; export const anomalyDeploymentData = [ { @@ -278,6 +283,49 @@ export const mockedQueryResultPayload = { ], }; +export const mockedQueryResultPayloadCoresTotal = { + metricId: '13_system_metrics_kubernetes_container_cores_total', + result: [ + { + metric: {}, + values: [ + [1563272065.589, '9.396484375'], + [1563272125.589, '9.333984375'], + [1563272185.589, '9.333984375'], + [1563272245.589, '9.333984375'], + [1563272305.589, '9.333984375'], + [1563272365.589, '9.333984375'], + [1563272425.589, '9.38671875'], + [1563272485.589, '9.333984375'], + [1563272545.589, '9.333984375'], + [1563272605.589, '9.333984375'], + [1563272665.589, '9.333984375'], + [1563272725.589, '9.333984375'], + [1563272785.589, '9.396484375'], + [1563272845.589, '9.333984375'], + [1563272905.589, '9.333984375'], + [1563272965.589, '9.3984375'], + [1563273025.589, '9.337890625'], + [1563273085.589, '9.34765625'], + [1563273145.589, '9.337890625'], + [1563273205.589, '9.337890625'], + [1563273265.589, '9.337890625'], + [1563273325.589, '9.337890625'], + [1563273385.589, '9.337890625'], + [1563273445.589, '9.337890625'], + [1563273505.589, '9.337890625'], + [1563273565.589, '9.337890625'], + [1563273625.589, '9.337890625'], + [1563273685.589, '9.337890625'], + [1563273745.589, '9.337890625'], + [1563273805.589, '9.337890625'], + [1563273865.589, '9.390625'], + [1563273925.589, '9.390625'], + ], + }, + ], +}; + export const metricsGroupsAPIResponse = [ { group: 'System metrics (Kubernetes)', @@ -460,3 +508,130 @@ export const dashboardGitResponse = [ path: '.gitlab/dashboards/dashboard_2.yml', }, ]; + +export const graphDataPrometheusQuery = { + title: 'Super Chart A2', + type: 'single-stat', + weight: 2, + metrics: [ + { + id: 'metric_a1', + metricId: '2', + query: 'max(go_memstats_alloc_bytes{job="prometheus"}) by (job) /1024/1024', + unit: 'MB', + label: 'Total Consumption', + metric_id: 2, + prometheus_endpoint_path: + '/root/kubernetes-gke-project/environments/35/prometheus/api/v1/query?query=max%28go_memstats_alloc_bytes%7Bjob%3D%22prometheus%22%7D%29+by+%28job%29+%2F1024%2F1024', + result: [ + { + metric: { job: 'prometheus' }, + value: ['2019-06-26T21:03:20.881Z', 91], + }, + ], + }, + ], +}; + +export const graphDataPrometheusQueryRange = { + title: 'Super Chart A1', + type: 'area-chart', + weight: 2, + metrics: [ + { + id: 'metric_a1', + metricId: '2', + query_range: + 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) /1024/1024/1024', + unit: 'MB', + label: 'Total Consumption', + metric_id: 2, + prometheus_endpoint_path: + '/root/kubernetes-gke-project/environments/35/prometheus/api/v1/query?query=max%28go_memstats_alloc_bytes%7Bjob%3D%22prometheus%22%7D%29+by+%28job%29+%2F1024%2F1024', + result: [ + { + metric: {}, + values: [[1495700554.925, '8.0390625'], [1495700614.925, '8.0390625']], + }, + ], + }, + ], +}; + +export const graphDataPrometheusQueryRangeMultiTrack = { + title: 'Super Chart A3', + type: 'heatmap', + weight: 3, + x_label: 'Status Code', + y_label: 'Time', + metrics: [ + { + metricId: '1', + id: 'response_metrics_nginx_ingress_throughput_status_code', + query_range: + 'sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[60m])) by (status_code)', + unit: 'req / sec', + label: 'Status Code', + metric_id: 1, + prometheus_endpoint_path: + '/root/rails_nodb/environments/3/prometheus/api/v1/query_range?query=sum%28rate%28nginx_upstream_responses_total%7Bupstream%3D~%22%25%7Bkube_namespace%7D-%25%7Bci_environment_slug%7D-.%2A%22%7D%5B2m%5D%29%29+by+%28status_code%29', + result: [ + { + metric: { status_code: '1xx' }, + values: [ + ['2019-08-30T15:00:00.000Z', 0], + ['2019-08-30T16:00:00.000Z', 2], + ['2019-08-30T17:00:00.000Z', 0], + ['2019-08-30T18:00:00.000Z', 0], + ['2019-08-30T19:00:00.000Z', 0], + ['2019-08-30T20:00:00.000Z', 3], + ], + }, + { + metric: { status_code: '2xx' }, + values: [ + ['2019-08-30T15:00:00.000Z', 1], + ['2019-08-30T16:00:00.000Z', 3], + ['2019-08-30T17:00:00.000Z', 6], + ['2019-08-30T18:00:00.000Z', 10], + ['2019-08-30T19:00:00.000Z', 8], + ['2019-08-30T20:00:00.000Z', 6], + ], + }, + { + metric: { status_code: '3xx' }, + values: [ + ['2019-08-30T15:00:00.000Z', 1], + ['2019-08-30T16:00:00.000Z', 2], + ['2019-08-30T17:00:00.000Z', 3], + ['2019-08-30T18:00:00.000Z', 3], + ['2019-08-30T19:00:00.000Z', 2], + ['2019-08-30T20:00:00.000Z', 1], + ], + }, + { + metric: { status_code: '4xx' }, + values: [ + ['2019-08-30T15:00:00.000Z', 2], + ['2019-08-30T16:00:00.000Z', 0], + ['2019-08-30T17:00:00.000Z', 0], + ['2019-08-30T18:00:00.000Z', 2], + ['2019-08-30T19:00:00.000Z', 0], + ['2019-08-30T20:00:00.000Z', 2], + ], + }, + { + metric: { status_code: '5xx' }, + values: [ + ['2019-08-30T15:00:00.000Z', 0], + ['2019-08-30T16:00:00.000Z', 1], + ['2019-08-30T17:00:00.000Z', 0], + ['2019-08-30T18:00:00.000Z', 0], + ['2019-08-30T19:00:00.000Z', 0], + ['2019-08-30T20:00:00.000Z', 2], + ], + }, + ], + }, + ], +}; diff --git a/spec/frontend/sidebar/assignees_spec.js b/spec/frontend/sidebar/assignees_spec.js new file mode 100644 index 00000000000..14b6da10991 --- /dev/null +++ b/spec/frontend/sidebar/assignees_spec.js @@ -0,0 +1,200 @@ +import { mount } from '@vue/test-utils'; +import { trimText } from 'helpers/text_helper'; +import Assignee from '~/sidebar/components/assignees/assignees.vue'; +import UsersMock from './mock_data'; +import UsersMockHelper from '../helpers/user_mock_data_helper'; + +describe('Assignee component', () => { + const getDefaultProps = () => ({ + rootPath: 'http://localhost:3000', + users: [], + editable: false, + }); + let wrapper; + + const createWrapper = (propsData = getDefaultProps()) => { + wrapper = mount(Assignee, { + propsData, + sync: false, + attachToDocument: true, + }); + }; + + const findComponentTextNoUsers = () => wrapper.find('.assign-yourself'); + const findCollapsedChildren = () => wrapper.findAll('.sidebar-collapsed-icon > *'); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('No assignees/users', () => { + it('displays no assignee icon when collapsed', () => { + createWrapper(); + const collapsedChildren = findCollapsedChildren(); + + expect(collapsedChildren.length).toBe(1); + expect(collapsedChildren.at(0).attributes('aria-label')).toBe('None'); + expect(collapsedChildren.at(0).classes()).toContain('fa', 'fa-user'); + }); + + it('displays only "None" when no users are assigned and the issue is read-only', () => { + createWrapper(); + const componentTextNoUsers = trimText(findComponentTextNoUsers().text()); + + expect(componentTextNoUsers).toBe('None'); + expect(componentTextNoUsers).not.toContain('assign yourself'); + }); + + it('displays only "None" when no users are assigned and the issue can be edited', () => { + createWrapper({ + ...getDefaultProps(), + editable: true, + }); + const componentTextNoUsers = trimText(findComponentTextNoUsers().text()); + + expect(componentTextNoUsers).toContain('None'); + expect(componentTextNoUsers).toContain('assign yourself'); + }); + + it('emits the assign-self event when "assign yourself" is clicked', () => { + createWrapper({ + ...getDefaultProps(), + editable: true, + }); + + jest.spyOn(wrapper.vm, '$emit'); + wrapper.find('.assign-yourself .btn-link').trigger('click'); + + expect(wrapper.emitted('assign-self')).toBeTruthy(); + }); + }); + + describe('One assignee/user', () => { + it('displays one assignee icon when collapsed', () => { + createWrapper({ + ...getDefaultProps(), + users: [UsersMock.user], + }); + + const collapsedChildren = findCollapsedChildren(); + const assignee = collapsedChildren.at(0); + + expect(collapsedChildren.length).toBe(1); + expect(assignee.find('.avatar').attributes('src')).toBe(UsersMock.user.avatar); + expect(assignee.find('.avatar').attributes('alt')).toBe(`${UsersMock.user.name}'s avatar`); + + expect(trimText(assignee.find('.author').text())).toBe(UsersMock.user.name); + }); + }); + + describe('Two or more assignees/users', () => { + it('displays two assignee icons when collapsed', () => { + const users = UsersMockHelper.createNumberRandomUsers(2); + createWrapper({ + ...getDefaultProps(), + users, + }); + + const collapsedChildren = findCollapsedChildren(); + + expect(collapsedChildren.length).toBe(2); + + const first = collapsedChildren.at(0); + + expect(first.find('.avatar').attributes('src')).toBe(users[0].avatar); + expect(first.find('.avatar').attributes('alt')).toBe(`${users[0].name}'s avatar`); + + expect(trimText(first.find('.author').text())).toBe(users[0].name); + + const second = collapsedChildren.at(1); + + expect(second.find('.avatar').attributes('src')).toBe(users[1].avatar); + expect(second.find('.avatar').attributes('alt')).toBe(`${users[1].name}'s avatar`); + + expect(trimText(second.find('.author').text())).toBe(users[1].name); + }); + + it('displays one assignee icon and counter when collapsed', () => { + const users = UsersMockHelper.createNumberRandomUsers(3); + createWrapper({ + ...getDefaultProps(), + users, + }); + + const collapsedChildren = findCollapsedChildren(); + + expect(collapsedChildren.length).toBe(2); + + const first = collapsedChildren.at(0); + + expect(first.find('.avatar').attributes('src')).toBe(users[0].avatar); + expect(first.find('.avatar').attributes('alt')).toBe(`${users[0].name}'s avatar`); + + expect(trimText(first.find('.author').text())).toBe(users[0].name); + + const second = collapsedChildren.at(1); + + expect(trimText(second.find('.avatar-counter').text())).toBe('+2'); + }); + + it('Shows two assignees', () => { + const users = UsersMockHelper.createNumberRandomUsers(2); + createWrapper({ + ...getDefaultProps(), + users, + editable: true, + }); + + expect(wrapper.findAll('.user-item').length).toBe(users.length); + expect(wrapper.find('.user-list-more').exists()).toBe(false); + }); + + it('shows sorted assignee where "can merge" users are sorted first', () => { + const users = UsersMockHelper.createNumberRandomUsers(3); + users[0].can_merge = false; + users[1].can_merge = false; + users[2].can_merge = true; + + createWrapper({ + ...getDefaultProps(), + users, + editable: true, + }); + + expect(wrapper.vm.sortedAssigness[0].can_merge).toBe(true); + }); + + it('passes the sorted assignees to the uncollapsed-assignee-list', () => { + const users = UsersMockHelper.createNumberRandomUsers(3); + users[0].can_merge = false; + users[1].can_merge = false; + users[2].can_merge = true; + + createWrapper({ + ...getDefaultProps(), + users, + }); + + const userItems = wrapper.findAll('.user-list .user-item a'); + + expect(userItems.length).toBe(3); + expect(userItems.at(0).attributes('data-original-title')).toBe(users[2].name); + }); + + it('passes the sorted assignees to the collapsed-assignee-list', () => { + const users = UsersMockHelper.createNumberRandomUsers(3); + users[0].can_merge = false; + users[1].can_merge = false; + users[2].can_merge = true; + + createWrapper({ + ...getDefaultProps(), + users, + }); + + const collapsedButton = wrapper.find('.sidebar-collapsed-user button'); + + expect(trimText(collapsedButton.text())).toBe(users[2].name); + }); + }); +}); diff --git a/spec/frontend/sidebar/mock_data.js b/spec/frontend/sidebar/mock_data.js new file mode 100644 index 00000000000..3ee97b978fd --- /dev/null +++ b/spec/frontend/sidebar/mock_data.js @@ -0,0 +1,213 @@ +const RESPONSE_MAP = { + GET: { + '/gitlab-org/gitlab-shell/issues/5.json': { + id: 45, + iid: 5, + author_id: 23, + description: 'Nulla ullam commodi delectus adipisci quis sit.', + lock_version: null, + milestone_id: 21, + position: 0, + state: 'closed', + title: 'Vel et nulla voluptatibus corporis dolor iste saepe laborum.', + updated_by_id: 1, + created_at: '2017-02-02T21: 49: 49.664Z', + updated_at: '2017-05-03T22: 26: 03.760Z', + time_estimate: 0, + total_time_spent: 0, + human_time_estimate: null, + human_total_time_spent: null, + branch_name: null, + confidential: false, + assignees: [ + { + name: 'User 0', + username: 'user0', + id: 22, + state: 'active', + avatar_url: + 'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon', + web_url: 'http: //localhost:3001/user0', + }, + { + name: 'Marguerite Bartell', + username: 'tajuana', + id: 18, + state: 'active', + avatar_url: + 'https://www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon', + web_url: 'http: //localhost:3001/tajuana', + }, + { + name: 'Laureen Ritchie', + username: 'michaele.will', + id: 16, + state: 'active', + avatar_url: + 'https://www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon', + web_url: 'http: //localhost:3001/michaele.will', + }, + ], + due_date: null, + moved_to_id: null, + project_id: 4, + weight: null, + milestone: { + id: 21, + iid: 1, + project_id: 4, + title: 'v0.0', + description: 'Molestiae commodi laboriosam odio sunt eaque reprehenderit.', + state: 'active', + created_at: '2017-02-02T21: 49: 30.530Z', + updated_at: '2017-02-02T21: 49: 30.530Z', + due_date: null, + start_date: null, + }, + labels: [], + }, + '/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar_extras': { + assignees: [ + { + name: 'User 0', + username: 'user0', + id: 22, + state: 'active', + avatar_url: + 'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon', + web_url: 'http://localhost:3001/user0', + }, + { + name: 'Marguerite Bartell', + username: 'tajuana', + id: 18, + state: 'active', + avatar_url: + 'https://www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon', + web_url: 'http://localhost:3001/tajuana', + }, + { + name: 'Laureen Ritchie', + username: 'michaele.will', + id: 16, + state: 'active', + avatar_url: + 'https://www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon', + web_url: 'http://localhost:3001/michaele.will', + }, + ], + human_time_estimate: null, + human_total_time_spent: null, + participants: [ + { + name: 'User 0', + username: 'user0', + id: 22, + state: 'active', + avatar_url: + 'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon', + web_url: 'http://localhost:3001/user0', + }, + { + name: 'Marguerite Bartell', + username: 'tajuana', + id: 18, + state: 'active', + avatar_url: + 'https://www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon', + web_url: 'http://localhost:3001/tajuana', + }, + { + name: 'Laureen Ritchie', + username: 'michaele.will', + id: 16, + state: 'active', + avatar_url: + 'https://www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon', + web_url: 'http://localhost:3001/michaele.will', + }, + ], + subscribed: true, + time_estimate: 0, + total_time_spent: 0, + }, + '/autocomplete/projects?project_id=15': [ + { + id: 0, + name_with_namespace: 'No project', + }, + { + id: 20, + name_with_namespace: '<img src=x onerror=alert(document.domain)> foo / bar', + }, + ], + }, + PUT: { + '/gitlab-org/gitlab-shell/issues/5.json': { + data: {}, + }, + }, + POST: { + '/gitlab-org/gitlab-shell/issues/5/move': { + id: 123, + iid: 5, + author_id: 1, + description: 'some description', + lock_version: 5, + milestone_id: null, + state: 'opened', + title: 'some title', + updated_by_id: 1, + created_at: '2017-06-27T19:54:42.437Z', + updated_at: '2017-08-18T03:39:49.222Z', + time_estimate: 0, + total_time_spent: 0, + human_time_estimate: null, + human_total_time_spent: null, + branch_name: null, + confidential: false, + assignees: [], + due_date: null, + moved_to_id: null, + project_id: 7, + milestone: null, + labels: [], + web_url: '/root/some-project/issues/5', + }, + '/gitlab-org/gitlab-shell/issues/5/toggle_subscription': {}, + }, +}; + +const mockData = { + responseMap: RESPONSE_MAP, + mediator: { + endpoint: '/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar_extras', + toggleSubscriptionEndpoint: '/gitlab-org/gitlab-shell/issues/5/toggle_subscription', + moveIssueEndpoint: '/gitlab-org/gitlab-shell/issues/5/move', + projectsAutocompleteEndpoint: '/autocomplete/projects?project_id=15', + editable: true, + currentUser: { + id: 1, + name: 'Administrator', + username: 'root', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + }, + rootPath: '/', + fullPath: '/gitlab-org/gitlab-shell', + }, + time: { + time_estimate: 3600, + total_time_spent: 0, + human_time_estimate: '1h', + human_total_time_spent: null, + }, + user: { + avatar: 'https://gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + id: 1, + name: 'Administrator', + username: 'root', + }, +}; + +export default mockData; diff --git a/spec/graphql/mutations/issues/set_confidential_spec.rb b/spec/graphql/mutations/issues/set_confidential_spec.rb new file mode 100644 index 00000000000..05b787eb5ca --- /dev/null +++ b/spec/graphql/mutations/issues/set_confidential_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Mutations::Issues::SetConfidential do + let(:issue) { create(:issue) } + let(:user) { create(:user) } + subject(:mutation) { described_class.new(object: nil, context: { current_user: user }) } + + describe '#resolve' do + let(:confidential) { true } + let(:mutated_issue) { subject[:issue] } + subject { mutation.resolve(project_path: issue.project.full_path, iid: issue.iid, confidential: confidential) } + + it 'raises an error if the resource is not accessible to the user' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + + context 'when the user can update the issue' do + before do + issue.project.add_developer(user) + end + + it 'returns the issue as confidential' do + expect(mutated_issue).to eq(issue) + expect(mutated_issue.confidential).to be_truthy + expect(subject[:errors]).to be_empty + end + + context 'when passing confidential as false' do + let(:confidential) { false } + + it 'updates the issue confidentiality to false' do + expect(mutated_issue.confidential).to be_falsey + end + end + end + end +end diff --git a/spec/javascripts/monitoring/mock_data.js b/spec/javascripts/monitoring/mock_data.js index f59c4ee4264..c80401e8c1d 100644 --- a/spec/javascripts/monitoring/mock_data.js +++ b/spec/javascripts/monitoring/mock_data.js @@ -1,226 +1,5 @@ -import { - anomalyMockGraphData as importedAnomalyMockGraphData, - metricsGroupsAPIResponse as importedMetricsGroupsAPIResponse, - environmentData as importedEnvironmentData, - dashboardGitResponse as importedDashboardGitResponse, -} from '../../frontend/monitoring/mock_data'; +// No new code should be added to this file. Instead, modify the +// file this one re-exports from. For more detail about why, see: +// https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/31349 -export const anomalyMockGraphData = importedAnomalyMockGraphData; -export const metricsGroupsAPIResponse = importedMetricsGroupsAPIResponse; -export const environmentData = importedEnvironmentData; -export const dashboardGitResponse = importedDashboardGitResponse; - -export const mockApiEndpoint = `${gl.TEST_HOST}/monitoring/mock`; - -export const mockedQueryResultPayload = { - metricId: '17_system_metrics_kubernetes_container_memory_average', - result: [ - { - metric: {}, - values: [ - [1563272065.589, '10.396484375'], - [1563272125.589, '10.333984375'], - [1563272185.589, '10.333984375'], - [1563272245.589, '10.333984375'], - [1563272305.589, '10.333984375'], - [1563272365.589, '10.333984375'], - [1563272425.589, '10.38671875'], - [1563272485.589, '10.333984375'], - [1563272545.589, '10.333984375'], - [1563272605.589, '10.333984375'], - [1563272665.589, '10.333984375'], - [1563272725.589, '10.333984375'], - [1563272785.589, '10.396484375'], - [1563272845.589, '10.333984375'], - [1563272905.589, '10.333984375'], - [1563272965.589, '10.3984375'], - [1563273025.589, '10.337890625'], - [1563273085.589, '10.34765625'], - [1563273145.589, '10.337890625'], - [1563273205.589, '10.337890625'], - [1563273265.589, '10.337890625'], - [1563273325.589, '10.337890625'], - [1563273385.589, '10.337890625'], - [1563273445.589, '10.337890625'], - [1563273505.589, '10.337890625'], - [1563273565.589, '10.337890625'], - [1563273625.589, '10.337890625'], - [1563273685.589, '10.337890625'], - [1563273745.589, '10.337890625'], - [1563273805.589, '10.337890625'], - [1563273865.589, '10.390625'], - [1563273925.589, '10.390625'], - ], - }, - ], -}; - -export const mockedQueryResultPayloadCoresTotal = { - metricId: '13_system_metrics_kubernetes_container_cores_total', - result: [ - { - metric: {}, - values: [ - [1563272065.589, '9.396484375'], - [1563272125.589, '9.333984375'], - [1563272185.589, '9.333984375'], - [1563272245.589, '9.333984375'], - [1563272305.589, '9.333984375'], - [1563272365.589, '9.333984375'], - [1563272425.589, '9.38671875'], - [1563272485.589, '9.333984375'], - [1563272545.589, '9.333984375'], - [1563272605.589, '9.333984375'], - [1563272665.589, '9.333984375'], - [1563272725.589, '9.333984375'], - [1563272785.589, '9.396484375'], - [1563272845.589, '9.333984375'], - [1563272905.589, '9.333984375'], - [1563272965.589, '9.3984375'], - [1563273025.589, '9.337890625'], - [1563273085.589, '9.34765625'], - [1563273145.589, '9.337890625'], - [1563273205.589, '9.337890625'], - [1563273265.589, '9.337890625'], - [1563273325.589, '9.337890625'], - [1563273385.589, '9.337890625'], - [1563273445.589, '9.337890625'], - [1563273505.589, '9.337890625'], - [1563273565.589, '9.337890625'], - [1563273625.589, '9.337890625'], - [1563273685.589, '9.337890625'], - [1563273745.589, '9.337890625'], - [1563273805.589, '9.337890625'], - [1563273865.589, '9.390625'], - [1563273925.589, '9.390625'], - ], - }, - ], -}; - -export const graphDataPrometheusQuery = { - title: 'Super Chart A2', - type: 'single-stat', - weight: 2, - metrics: [ - { - id: 'metric_a1', - metricId: '2', - query: 'max(go_memstats_alloc_bytes{job="prometheus"}) by (job) /1024/1024', - unit: 'MB', - label: 'Total Consumption', - metric_id: 2, - prometheus_endpoint_path: - '/root/kubernetes-gke-project/environments/35/prometheus/api/v1/query?query=max%28go_memstats_alloc_bytes%7Bjob%3D%22prometheus%22%7D%29+by+%28job%29+%2F1024%2F1024', - result: [ - { - metric: { job: 'prometheus' }, - value: ['2019-06-26T21:03:20.881Z', 91], - }, - ], - }, - ], -}; - -export const graphDataPrometheusQueryRange = { - title: 'Super Chart A1', - type: 'area-chart', - weight: 2, - metrics: [ - { - id: 'metric_a1', - metricId: '2', - query_range: - 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) /1024/1024/1024', - unit: 'MB', - label: 'Total Consumption', - metric_id: 2, - prometheus_endpoint_path: - '/root/kubernetes-gke-project/environments/35/prometheus/api/v1/query?query=max%28go_memstats_alloc_bytes%7Bjob%3D%22prometheus%22%7D%29+by+%28job%29+%2F1024%2F1024', - result: [ - { - metric: {}, - values: [[1495700554.925, '8.0390625'], [1495700614.925, '8.0390625']], - }, - ], - }, - ], -}; - -export const graphDataPrometheusQueryRangeMultiTrack = { - title: 'Super Chart A3', - type: 'heatmap', - weight: 3, - x_label: 'Status Code', - y_label: 'Time', - metrics: [ - { - metricId: '1', - id: 'response_metrics_nginx_ingress_throughput_status_code', - query_range: - 'sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[60m])) by (status_code)', - unit: 'req / sec', - label: 'Status Code', - metric_id: 1, - prometheus_endpoint_path: - '/root/rails_nodb/environments/3/prometheus/api/v1/query_range?query=sum%28rate%28nginx_upstream_responses_total%7Bupstream%3D~%22%25%7Bkube_namespace%7D-%25%7Bci_environment_slug%7D-.%2A%22%7D%5B2m%5D%29%29+by+%28status_code%29', - result: [ - { - metric: { status_code: '1xx' }, - values: [ - ['2019-08-30T15:00:00.000Z', 0], - ['2019-08-30T16:00:00.000Z', 2], - ['2019-08-30T17:00:00.000Z', 0], - ['2019-08-30T18:00:00.000Z', 0], - ['2019-08-30T19:00:00.000Z', 0], - ['2019-08-30T20:00:00.000Z', 3], - ], - }, - { - metric: { status_code: '2xx' }, - values: [ - ['2019-08-30T15:00:00.000Z', 1], - ['2019-08-30T16:00:00.000Z', 3], - ['2019-08-30T17:00:00.000Z', 6], - ['2019-08-30T18:00:00.000Z', 10], - ['2019-08-30T19:00:00.000Z', 8], - ['2019-08-30T20:00:00.000Z', 6], - ], - }, - { - metric: { status_code: '3xx' }, - values: [ - ['2019-08-30T15:00:00.000Z', 1], - ['2019-08-30T16:00:00.000Z', 2], - ['2019-08-30T17:00:00.000Z', 3], - ['2019-08-30T18:00:00.000Z', 3], - ['2019-08-30T19:00:00.000Z', 2], - ['2019-08-30T20:00:00.000Z', 1], - ], - }, - { - metric: { status_code: '4xx' }, - values: [ - ['2019-08-30T15:00:00.000Z', 2], - ['2019-08-30T16:00:00.000Z', 0], - ['2019-08-30T17:00:00.000Z', 0], - ['2019-08-30T18:00:00.000Z', 2], - ['2019-08-30T19:00:00.000Z', 0], - ['2019-08-30T20:00:00.000Z', 2], - ], - }, - { - metric: { status_code: '5xx' }, - values: [ - ['2019-08-30T15:00:00.000Z', 0], - ['2019-08-30T16:00:00.000Z', 1], - ['2019-08-30T17:00:00.000Z', 0], - ['2019-08-30T18:00:00.000Z', 0], - ['2019-08-30T19:00:00.000Z', 0], - ['2019-08-30T20:00:00.000Z', 2], - ], - }, - ], - }, - ], -}; +export * from '../../frontend/monitoring/mock_data'; diff --git a/spec/javascripts/sidebar/assignees_spec.js b/spec/javascripts/sidebar/assignees_spec.js deleted file mode 100644 index a1df5389a38..00000000000 --- a/spec/javascripts/sidebar/assignees_spec.js +++ /dev/null @@ -1,248 +0,0 @@ -import Vue from 'vue'; -import Assignee from '~/sidebar/components/assignees/assignees.vue'; -import UsersMock from './mock_data'; -import UsersMockHelper from '../helpers/user_mock_data_helper'; - -describe('Assignee component', () => { - let component; - let AssigneeComponent; - - beforeEach(() => { - AssigneeComponent = Vue.extend(Assignee); - }); - - describe('No assignees/users', () => { - it('displays no assignee icon when collapsed', () => { - component = new AssigneeComponent({ - propsData: { - rootPath: 'http://localhost:3000', - users: [], - editable: false, - }, - }).$mount(); - - const collapsed = component.$el.querySelector('.sidebar-collapsed-icon'); - - expect(collapsed.childElementCount).toEqual(1); - expect(collapsed.children[0].getAttribute('aria-label')).toEqual('None'); - expect(collapsed.children[0].classList.contains('fa')).toEqual(true); - expect(collapsed.children[0].classList.contains('fa-user')).toEqual(true); - }); - - it('displays only "None" when no users are assigned and the issue is read-only', () => { - component = new AssigneeComponent({ - propsData: { - rootPath: 'http://localhost:3000', - users: [], - editable: false, - }, - }).$mount(); - const componentTextNoUsers = component.$el.querySelector('.assign-yourself').innerText.trim(); - - expect(componentTextNoUsers).toBe('None'); - expect(componentTextNoUsers.indexOf('assign yourself')).toEqual(-1); - }); - - it('displays only "None" when no users are assigned and the issue can be edited', () => { - component = new AssigneeComponent({ - propsData: { - rootPath: 'http://localhost:3000', - users: [], - editable: true, - }, - }).$mount(); - const componentTextNoUsers = component.$el.querySelector('.assign-yourself').innerText.trim(); - - expect(componentTextNoUsers.indexOf('None')).toEqual(0); - expect(componentTextNoUsers.indexOf('assign yourself')).toBeGreaterThan(0); - }); - - it('emits the assign-self event when "assign yourself" is clicked', () => { - component = new AssigneeComponent({ - propsData: { - rootPath: 'http://localhost:3000', - users: [], - editable: true, - }, - }).$mount(); - - spyOn(component, '$emit'); - component.$el.querySelector('.assign-yourself .btn-link').click(); - - expect(component.$emit).toHaveBeenCalledWith('assign-self'); - }); - }); - - describe('One assignee/user', () => { - it('displays one assignee icon when collapsed', () => { - component = new AssigneeComponent({ - propsData: { - rootPath: 'http://localhost:3000', - users: [UsersMock.user], - editable: false, - }, - }).$mount(); - - const collapsed = component.$el.querySelector('.sidebar-collapsed-icon'); - const assignee = collapsed.children[0]; - - expect(collapsed.childElementCount).toEqual(1); - expect(assignee.querySelector('.avatar').getAttribute('src')).toEqual(UsersMock.user.avatar); - expect(assignee.querySelector('.avatar').getAttribute('alt')).toEqual( - `${UsersMock.user.name}'s avatar`, - ); - - expect(assignee.querySelector('.author').innerText.trim()).toEqual(UsersMock.user.name); - }); - }); - - describe('Two or more assignees/users', () => { - it('has no "cannot merge" tooltip when every user can merge', () => { - const users = UsersMockHelper.createNumberRandomUsers(2); - users[0].can_merge = true; - users[1].can_merge = true; - - component = new AssigneeComponent({ - propsData: { - rootPath: 'http://localhost:3000/', - users, - editable: true, - issuableType: 'merge_request', - }, - }).$mount(); - - expect(component.collapsedTooltipTitle).not.toContain('cannot merge'); - }); - - it('displays two assignee icons when collapsed', () => { - const users = UsersMockHelper.createNumberRandomUsers(2); - component = new AssigneeComponent({ - propsData: { - rootPath: 'http://localhost:3000', - users, - editable: false, - }, - }).$mount(); - - const collapsed = component.$el.querySelector('.sidebar-collapsed-icon'); - - expect(collapsed.childElementCount).toEqual(2); - - const first = collapsed.children[0]; - - expect(first.querySelector('.avatar').getAttribute('src')).toEqual(users[0].avatar); - expect(first.querySelector('.avatar').getAttribute('alt')).toEqual( - `${users[0].name}'s avatar`, - ); - - expect(first.querySelector('.author').innerText.trim()).toEqual(users[0].name); - - const second = collapsed.children[1]; - - expect(second.querySelector('.avatar').getAttribute('src')).toEqual(users[1].avatar); - expect(second.querySelector('.avatar').getAttribute('alt')).toEqual( - `${users[1].name}'s avatar`, - ); - - expect(second.querySelector('.author').innerText.trim()).toEqual(users[1].name); - }); - - it('displays one assignee icon and counter when collapsed', () => { - const users = UsersMockHelper.createNumberRandomUsers(3); - component = new AssigneeComponent({ - propsData: { - rootPath: 'http://localhost:3000', - users, - editable: false, - }, - }).$mount(); - - const collapsed = component.$el.querySelector('.sidebar-collapsed-icon'); - - expect(collapsed.childElementCount).toEqual(2); - - const first = collapsed.children[0]; - - expect(first.querySelector('.avatar').getAttribute('src')).toEqual(users[0].avatar); - expect(first.querySelector('.avatar').getAttribute('alt')).toEqual( - `${users[0].name}'s avatar`, - ); - - expect(first.querySelector('.author').innerText.trim()).toEqual(users[0].name); - - const second = collapsed.children[1]; - - expect(second.querySelector('.avatar-counter').innerText.trim()).toEqual('+2'); - }); - - it('Shows two assignees', () => { - const users = UsersMockHelper.createNumberRandomUsers(2); - component = new AssigneeComponent({ - propsData: { - rootPath: 'http://localhost:3000', - users, - editable: true, - }, - }).$mount(); - - expect(component.$el.querySelectorAll('.user-item').length).toEqual(users.length); - expect(component.$el.querySelector('.user-list-more')).toBe(null); - }); - - it('shows sorted assignee where "can merge" users are sorted first', () => { - const users = UsersMockHelper.createNumberRandomUsers(3); - users[0].can_merge = false; - users[1].can_merge = false; - users[2].can_merge = true; - - component = new AssigneeComponent({ - propsData: { - rootPath: 'http://localhost:3000', - users, - editable: true, - }, - }).$mount(); - - expect(component.sortedAssigness[0].can_merge).toBe(true); - }); - - it('passes the sorted assignees to the uncollapsed-assignee-list', () => { - const users = UsersMockHelper.createNumberRandomUsers(3); - users[0].can_merge = false; - users[1].can_merge = false; - users[2].can_merge = true; - - component = new AssigneeComponent({ - propsData: { - rootPath: 'http://localhost:3000', - users, - editable: false, - }, - }).$mount(); - - const userItems = component.$el.querySelectorAll('.user-list .user-item a'); - - expect(userItems.length).toBe(3); - expect(userItems[0].dataset.originalTitle).toBe(users[2].name); - }); - - it('passes the sorted assignees to the collapsed-assignee-list', () => { - const users = UsersMockHelper.createNumberRandomUsers(3); - users[0].can_merge = false; - users[1].can_merge = false; - users[2].can_merge = true; - - component = new AssigneeComponent({ - propsData: { - rootPath: 'http://localhost:3000', - users, - editable: false, - }, - }).$mount(); - - const collapsedButton = component.$el.querySelector('.sidebar-collapsed-user button'); - - expect(collapsedButton.innerText.trim()).toBe(users[2].name); - }); - }); -}); diff --git a/spec/javascripts/sidebar/mock_data.js b/spec/javascripts/sidebar/mock_data.js index 3ee97b978fd..c869ff96933 100644 --- a/spec/javascripts/sidebar/mock_data.js +++ b/spec/javascripts/sidebar/mock_data.js @@ -1,213 +1,7 @@ -const RESPONSE_MAP = { - GET: { - '/gitlab-org/gitlab-shell/issues/5.json': { - id: 45, - iid: 5, - author_id: 23, - description: 'Nulla ullam commodi delectus adipisci quis sit.', - lock_version: null, - milestone_id: 21, - position: 0, - state: 'closed', - title: 'Vel et nulla voluptatibus corporis dolor iste saepe laborum.', - updated_by_id: 1, - created_at: '2017-02-02T21: 49: 49.664Z', - updated_at: '2017-05-03T22: 26: 03.760Z', - time_estimate: 0, - total_time_spent: 0, - human_time_estimate: null, - human_total_time_spent: null, - branch_name: null, - confidential: false, - assignees: [ - { - name: 'User 0', - username: 'user0', - id: 22, - state: 'active', - avatar_url: - 'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon', - web_url: 'http: //localhost:3001/user0', - }, - { - name: 'Marguerite Bartell', - username: 'tajuana', - id: 18, - state: 'active', - avatar_url: - 'https://www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon', - web_url: 'http: //localhost:3001/tajuana', - }, - { - name: 'Laureen Ritchie', - username: 'michaele.will', - id: 16, - state: 'active', - avatar_url: - 'https://www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon', - web_url: 'http: //localhost:3001/michaele.will', - }, - ], - due_date: null, - moved_to_id: null, - project_id: 4, - weight: null, - milestone: { - id: 21, - iid: 1, - project_id: 4, - title: 'v0.0', - description: 'Molestiae commodi laboriosam odio sunt eaque reprehenderit.', - state: 'active', - created_at: '2017-02-02T21: 49: 30.530Z', - updated_at: '2017-02-02T21: 49: 30.530Z', - due_date: null, - start_date: null, - }, - labels: [], - }, - '/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar_extras': { - assignees: [ - { - name: 'User 0', - username: 'user0', - id: 22, - state: 'active', - avatar_url: - 'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon', - web_url: 'http://localhost:3001/user0', - }, - { - name: 'Marguerite Bartell', - username: 'tajuana', - id: 18, - state: 'active', - avatar_url: - 'https://www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon', - web_url: 'http://localhost:3001/tajuana', - }, - { - name: 'Laureen Ritchie', - username: 'michaele.will', - id: 16, - state: 'active', - avatar_url: - 'https://www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon', - web_url: 'http://localhost:3001/michaele.will', - }, - ], - human_time_estimate: null, - human_total_time_spent: null, - participants: [ - { - name: 'User 0', - username: 'user0', - id: 22, - state: 'active', - avatar_url: - 'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon', - web_url: 'http://localhost:3001/user0', - }, - { - name: 'Marguerite Bartell', - username: 'tajuana', - id: 18, - state: 'active', - avatar_url: - 'https://www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon', - web_url: 'http://localhost:3001/tajuana', - }, - { - name: 'Laureen Ritchie', - username: 'michaele.will', - id: 16, - state: 'active', - avatar_url: - 'https://www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon', - web_url: 'http://localhost:3001/michaele.will', - }, - ], - subscribed: true, - time_estimate: 0, - total_time_spent: 0, - }, - '/autocomplete/projects?project_id=15': [ - { - id: 0, - name_with_namespace: 'No project', - }, - { - id: 20, - name_with_namespace: '<img src=x onerror=alert(document.domain)> foo / bar', - }, - ], - }, - PUT: { - '/gitlab-org/gitlab-shell/issues/5.json': { - data: {}, - }, - }, - POST: { - '/gitlab-org/gitlab-shell/issues/5/move': { - id: 123, - iid: 5, - author_id: 1, - description: 'some description', - lock_version: 5, - milestone_id: null, - state: 'opened', - title: 'some title', - updated_by_id: 1, - created_at: '2017-06-27T19:54:42.437Z', - updated_at: '2017-08-18T03:39:49.222Z', - time_estimate: 0, - total_time_spent: 0, - human_time_estimate: null, - human_total_time_spent: null, - branch_name: null, - confidential: false, - assignees: [], - due_date: null, - moved_to_id: null, - project_id: 7, - milestone: null, - labels: [], - web_url: '/root/some-project/issues/5', - }, - '/gitlab-org/gitlab-shell/issues/5/toggle_subscription': {}, - }, -}; +// No new code should be added to this file. Instead, modify the +// file this one re-exports from. For more detail about why, see: +// https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/31349 -const mockData = { - responseMap: RESPONSE_MAP, - mediator: { - endpoint: '/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar_extras', - toggleSubscriptionEndpoint: '/gitlab-org/gitlab-shell/issues/5/toggle_subscription', - moveIssueEndpoint: '/gitlab-org/gitlab-shell/issues/5/move', - projectsAutocompleteEndpoint: '/autocomplete/projects?project_id=15', - editable: true, - currentUser: { - id: 1, - name: 'Administrator', - username: 'root', - avatar_url: - 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', - }, - rootPath: '/', - fullPath: '/gitlab-org/gitlab-shell', - }, - time: { - time_estimate: 3600, - total_time_spent: 0, - human_time_estimate: '1h', - human_total_time_spent: null, - }, - user: { - avatar: 'https://gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', - id: 1, - name: 'Administrator', - username: 'root', - }, -}; +import mockData from '../../../spec/frontend/sidebar/mock_data'; export default mockData; diff --git a/spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb b/spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb index c7a5ac783b3..caa84faa9c1 100644 --- a/spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb +++ b/spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb @@ -38,13 +38,29 @@ describe Gitlab::Ci::Build::Prerequisite::KubernetesNamespace do .and_return(double(execute: kubernetes_namespace)) end - it { is_expected.to be_falsey } - - context 'and the service_account_token is blank' do - let(:kubernetes_namespace) { instance_double(Clusters::KubernetesNamespace, service_account_token: nil) } + context 'and the knative version role binding is missing' do + before do + allow(Clusters::KnativeVersionRoleBindingFinder).to receive(:new) + .and_return(double(execute: nil)) + end it { is_expected.to be_truthy } end + + context 'and the knative version role binding already exists' do + before do + allow(Clusters::KnativeVersionRoleBindingFinder).to receive(:new) + .and_return(double(execute: true)) + end + + it { is_expected.to be_falsey } + + context 'and the service_account_token is blank' do + let(:kubernetes_namespace) { instance_double(Clusters::KubernetesNamespace, service_account_token: nil) } + + it { is_expected.to be_truthy } + end + end end end @@ -115,6 +131,24 @@ describe Gitlab::Ci::Build::Prerequisite::KubernetesNamespace do subject end end + + context 'knative version role binding is missing' do + before do + allow(Clusters::KubernetesNamespaceFinder).to receive(:new) + .and_return(double(execute: kubernetes_namespace)) + allow(Clusters::KnativeVersionRoleBindingFinder).to receive(:new) + .and_return(double(execute: nil)) + end + + it 'creates the knative version role binding' do + expect(Clusters::Kubernetes::CreateOrUpdateNamespaceService) + .to receive(:new) + .with(cluster: cluster, kubernetes_namespace: kubernetes_namespace) + .and_return(service) + + subject + end + end end context 'completion is not required' do diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb index 6c9467916de..0ab3e513e24 100644 --- a/spec/lib/gitlab/git/commit_spec.rb +++ b/spec/lib/gitlab/git/commit_spec.rb @@ -17,13 +17,13 @@ describe Gitlab::Git::Commit, :seed_helper do @committer = { email: 'mike@smith.com', name: "Mike Smith", - time: Time.now + time: Time.new(2000, 1, 1, 0, 0, 0, "+08:00") } @author = { email: 'john@smith.com', name: "John Smith", - time: Time.now + time: Time.new(2000, 1, 1, 0, 0, 0, "-08:00") } @parents = [rugged_repo.head.target] @@ -48,7 +48,7 @@ describe Gitlab::Git::Commit, :seed_helper do it { expect(@commit.id).to eq(@raw_commit.oid) } it { expect(@commit.sha).to eq(@raw_commit.oid) } it { expect(@commit.safe_message).to eq(@raw_commit.message) } - it { expect(@commit.created_at).to eq(@raw_commit.author[:time]) } + it { expect(@commit.created_at).to eq(@raw_commit.committer[:time]) } it { expect(@commit.date).to eq(@raw_commit.committer[:time]) } it { expect(@commit.author_email).to eq(@author[:email]) } it { expect(@commit.author_name).to eq(@author[:name]) } @@ -79,13 +79,27 @@ describe Gitlab::Git::Commit, :seed_helper do it { expect(commit.id).to eq(id) } it { expect(commit.sha).to eq(id) } it { expect(commit.safe_message).to eq(body) } - it { expect(commit.created_at).to eq(Time.at(committer.date.seconds)) } + it { expect(commit.created_at).to eq(Time.at(committer.date.seconds).utc) } it { expect(commit.author_email).to eq(author.email) } it { expect(commit.author_name).to eq(author.name) } it { expect(commit.committer_name).to eq(committer.name) } it { expect(commit.committer_email).to eq(committer.email) } it { expect(commit.parent_ids).to eq(gitaly_commit.parent_ids) } + context 'non-UTC dates' do + let(:seconds) { Time.now.to_i } + + it 'sets timezones correctly' do + gitaly_commit.author.date.seconds = seconds + gitaly_commit.author.timezone = '-0800' + gitaly_commit.committer.date.seconds = seconds + gitaly_commit.committer.timezone = '+0800' + + expect(commit.authored_date).to eq(Time.at(seconds, in: '-08:00')) + expect(commit.committed_date).to eq(Time.at(seconds, in: '+08:00')) + end + end + context 'body_size != body.size' do let(:body) { (+"").force_encoding('ASCII-8BIT') } diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 916f5536ebd..95435c6dabd 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1197,6 +1197,54 @@ describe Ci::Build do end end + describe '#expanded_kubernetes_namespace' do + let(:build) { create(:ci_build, environment: environment, options: options) } + + subject { build.expanded_kubernetes_namespace } + + context 'environment and namespace are not set' do + let(:environment) { nil } + let(:options) { nil } + + it { is_expected.to be_nil } + end + + context 'environment is specified' do + let(:environment) { 'production' } + + context 'namespace is not set' do + let(:options) { nil } + + it { is_expected.to be_nil } + end + + context 'namespace is provided' do + let(:options) do + { + environment: { + name: environment, + kubernetes: { + namespace: namespace + } + } + } + end + + context 'with a static value' do + let(:namespace) { 'production' } + + it { is_expected.to eq namespace } + end + + context 'with a dynamic value' do + let(:namespace) { 'deploy-$CI_COMMIT_REF_NAME'} + + it { is_expected.to eq 'deploy-master' } + end + end + end + end + describe '#starts_environment?' do subject { build.starts_environment? } @@ -2987,6 +3035,32 @@ describe Ci::Build do end end + describe '#deployment_variables' do + let(:build) { create(:ci_build, environment: environment) } + let(:environment) { 'production' } + let(:kubernetes_namespace) { 'namespace' } + let(:project_variables) { double } + + subject { build.deployment_variables(environment: environment) } + + before do + allow(build).to receive(:expanded_kubernetes_namespace) + .and_return(kubernetes_namespace) + + allow(build.project).to receive(:deployment_variables) + .with(environment: environment, kubernetes_namespace: kubernetes_namespace) + .and_return(project_variables) + end + + it { is_expected.to eq(project_variables) } + + context 'environment is nil' do + let(:environment) { nil } + + it { is_expected.to be_empty } + end + end + describe '#scoped_variables_hash' do context 'when overriding CI variables' do before do diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb index d53fc32cfef..4271cf9f1b3 100644 --- a/spec/models/clusters/platforms/kubernetes_spec.rb +++ b/spec/models/clusters/platforms/kubernetes_spec.rb @@ -290,6 +290,26 @@ describe Clusters::Platforms::Kubernetes do it { is_expected.to include(key: 'KUBE_TOKEN', value: platform.token, public: false, masked: true) } it { is_expected.to include(key: 'KUBE_NAMESPACE', value: namespace) } it { is_expected.to include(key: 'KUBECONFIG', value: kubeconfig, public: false, file: true) } + + context 'custom namespace is provided' do + let(:custom_namespace) { 'custom-namespace' } + + subject do + platform.predefined_variables( + project: project, + environment_name: environment_name, + kubernetes_namespace: custom_namespace + ) + end + + before do + allow(platform).to receive(:kubeconfig).with(custom_namespace).and_return(kubeconfig) + end + + it { is_expected.to include(key: 'KUBE_TOKEN', value: platform.token, public: false, masked: true) } + it { is_expected.to include(key: 'KUBE_NAMESPACE', value: custom_namespace) } + it { is_expected.to include(key: 'KUBECONFIG', value: kubeconfig, public: false, file: true) } + end end end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 839c4cadb5e..37c8484d69b 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -359,7 +359,7 @@ eos it { expect(data).to be_a(Hash) } it { expect(data[:message]).to include('adds bar folder and branch-test text file to check Repository merged_to_root_ref method') } - it { expect(data[:timestamp]).to eq('2016-09-27T14:37:46Z') } + it { expect(data[:timestamp]).to eq('2016-09-27T14:37:46+00:00') } it { expect(data[:added]).to contain_exactly("bar/branch-test.txt") } it { expect(data[:modified]).to eq([]) } it { expect(data[:removed]).to eq([]) } diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index 45cd2768708..d84a8665dc8 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -106,6 +106,40 @@ describe Milestone do end end + describe '#merge_requests_enabled?' do + context "per project" do + it "is true for projects with MRs enabled" do + project = create(:project, :merge_requests_enabled) + milestone = create(:milestone, project: project) + + expect(milestone.merge_requests_enabled?).to be(true) + end + + it "is false for projects with MRs disabled" do + project = create(:project, :repository_enabled, :merge_requests_disabled) + milestone = create(:milestone, project: project) + + expect(milestone.merge_requests_enabled?).to be(false) + end + + it "is false for projects with repository disabled" do + project = create(:project, :repository_disabled) + milestone = create(:milestone, project: project) + + expect(milestone.merge_requests_enabled?).to be(false) + end + end + + context "per group" do + let(:group) { create(:group) } + let(:milestone) { create(:milestone, group: group) } + + it "is always true for groups, for performance reasons" do + expect(milestone.merge_requests_enabled?).to be(true) + end + end + end + describe "unique milestone title" do context "per project" do it "does not accept the same title in a project twice" do diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index b1f88c4530e..37604a557ea 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -2765,8 +2765,9 @@ describe Project do describe '#deployment_variables' do let(:project) { create(:project) } let(:environment) { 'production' } + let(:namespace) { 'namespace' } - subject { project.deployment_variables(environment: environment) } + subject { project.deployment_variables(environment: environment, kubernetes_namespace: namespace) } before do expect(project).to receive(:deployment_platform).with(environment: environment) @@ -2785,7 +2786,7 @@ describe Project do before do expect(deployment_platform).to receive(:predefined_variables) - .with(project: project, environment_name: environment) + .with(project: project, environment_name: environment, kubernetes_namespace: namespace) .and_return(platform_variables) end diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb index 675b06b057c..eda2f6d854f 100644 --- a/spec/requests/api/branches_spec.rb +++ b/spec/requests/api/branches_spec.rb @@ -131,7 +131,7 @@ describe API::Branches do end new_branch_name = 'protected-branch' - CreateBranchService.new(project, current_user).execute(new_branch_name, 'master') + ::Branches::CreateService.new(project, current_user).execute(new_branch_name, 'master') create(:protected_branch, name: new_branch_name, project: project) expect do diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb index ec18156f49f..378441220c7 100644 --- a/spec/requests/api/files_spec.rb +++ b/spec/requests/api/files_spec.rb @@ -315,11 +315,11 @@ describe API::Files do expect(range['commit']['message']) .to eq("Files, encoding and much more\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n") - expect(range['commit']['authored_date']).to eq('2014-02-27T08:14:56.000Z') + expect(range['commit']['authored_date']).to eq('2014-02-27T10:14:56.000+02:00') expect(range['commit']['author_name']).to eq('Dmitriy Zaporozhets') expect(range['commit']['author_email']).to eq('dmitriy.zaporozhets@gmail.com') - expect(range['commit']['committed_date']).to eq('2014-02-27T08:14:56.000Z') + expect(range['commit']['committed_date']).to eq('2014-02-27T10:14:56.000+02:00') expect(range['commit']['committer_name']).to eq('Dmitriy Zaporozhets') expect(range['commit']['committer_email']).to eq('dmitriy.zaporozhets@gmail.com') end diff --git a/spec/requests/api/graphql/mutations/issues/set_confidential_spec.rb b/spec/requests/api/graphql/mutations/issues/set_confidential_spec.rb new file mode 100644 index 00000000000..4d0bb59b030 --- /dev/null +++ b/spec/requests/api/graphql/mutations/issues/set_confidential_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Setting an issue as confidential' do + include GraphqlHelpers + + let(:current_user) { create(:user) } + let(:issue) { create(:issue) } + let(:project) { issue.project } + let(:input) { { confidential: true } } + + let(:mutation) do + variables = { + project_path: project.full_path, + iid: issue.iid.to_s + } + graphql_mutation(:issue_set_confidential, variables.merge(input), + <<-QL.strip_heredoc + clientMutationId + errors + issue { + iid + confidential + } + QL + ) + end + + def mutation_response + graphql_mutation_response(:issue_set_confidential) + end + + before do + project.add_developer(current_user) + end + + it 'returns an error if the user is not allowed to update the issue' do + error = "The resource that you are attempting to access does not exist or you don't have permission to perform this action" + post_graphql_mutation(mutation, current_user: create(:user)) + + expect(graphql_errors).to include(a_hash_including('message' => error)) + end + + it 'updates the issue confidentiality' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['issue']['confidential']).to be_truthy + end +end diff --git a/spec/requests/user_avatar_spec.rb b/spec/requests/user_avatar_spec.rb new file mode 100644 index 00000000000..9451674161c --- /dev/null +++ b/spec/requests/user_avatar_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Loading a user avatar' do + let(:user) { create(:user, :with_avatar) } + + context 'when logged in' do + # The exact query count will vary depending on the 2FA settings of the + # instance, group, and user. Removing those extra 2FA queries in this case + # may not be a good idea, so we just set up the ideal case. + before do + stub_application_setting(require_two_factor_authentication: true) + + login_as(create(:user, :two_factor)) + end + + # One each for: current user, avatar user, and upload record + it 'only performs three SQL queries' do + get user.avatar_url # Skip queries on first application load + + expect(response).to have_gitlab_http_status(200) + expect { get user.avatar_url }.not_to exceed_query_limit(3) + end + end + + context 'when logged out' do + # One each for avatar user and upload record + it 'only performs two SQL queries' do + get user.avatar_url # Skip queries on first application load + + expect(response).to have_gitlab_http_status(200) + expect { get user.avatar_url }.not_to exceed_query_limit(2) + end + end +end diff --git a/spec/routing/instance_statistics_routing_spec.rb b/spec/routing/instance_statistics_routing_spec.rb index b94faabfa1d..48a3ac4695c 100644 --- a/spec/routing/instance_statistics_routing_spec.rb +++ b/spec/routing/instance_statistics_routing_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' describe 'Instance Statistics', 'routing' do include RSpec::Rails::RequestExampleGroup - it "routes '/-/instance_statistics' to conversational development index" do - expect(get('/-/instance_statistics')).to redirect_to('/-/instance_statistics/conversational_development_index') + it "routes '/-/instance_statistics' to dev ops score" do + expect(get('/-/instance_statistics')).to redirect_to('/-/instance_statistics/dev_ops_score') end end diff --git a/spec/services/create_branch_service_spec.rb b/spec/services/branches/create_service_spec.rb index 9661173c9e7..444491ed6f3 100644 --- a/spec/services/create_branch_service_spec.rb +++ b/spec/services/branches/create_service_spec.rb @@ -2,9 +2,9 @@ require 'spec_helper' -describe CreateBranchService do +describe Branches::CreateService do let(:user) { create(:user) } - let(:service) { described_class.new(project, user) } + subject(:service) { described_class.new(project, user) } describe '#execute' do context 'when repository is empty' do @@ -30,7 +30,7 @@ describe CreateBranchService do allow(project.repository).to receive(:add_branch).and_return(false) end - it 'retruns an error with the branch name' do + it 'returns an error with the branch name' do result = service.execute('my-feature', 'master') expect(result[:status]).to eq(:error) diff --git a/spec/services/delete_merged_branches_service_spec.rb b/spec/services/branches/delete_merged_service_spec.rb index dffc2bd93ee..962af8110f7 100644 --- a/spec/services/delete_merged_branches_service_spec.rb +++ b/spec/services/branches/delete_merged_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe DeleteMergedBranchesService do +describe Branches::DeleteMergedService do include ProjectForksHelper subject(:service) { described_class.new(project, project.owner) } diff --git a/spec/services/delete_branch_service_spec.rb b/spec/services/branches/delete_service_spec.rb index b8064c2cbc1..b4848978a6f 100644 --- a/spec/services/delete_branch_service_spec.rb +++ b/spec/services/branches/delete_service_spec.rb @@ -2,11 +2,11 @@ require 'spec_helper' -describe DeleteBranchService do +describe Branches::DeleteService do let(:project) { create(:project, :repository) } let(:repository) { project.repository } let(:user) { create(:user) } - let(:service) { described_class.new(project, user) } + subject(:service) { described_class.new(project, user) } shared_examples 'a deleted branch' do |branch_name| it 'removes the branch' do diff --git a/spec/services/branches/validate_new_service_spec.rb b/spec/services/branches/validate_new_service_spec.rb new file mode 100644 index 00000000000..460f28b5844 --- /dev/null +++ b/spec/services/branches/validate_new_service_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Branches::ValidateNewService do + let(:project) { create(:project, :repository) } + subject(:service) { described_class.new(project) } + + describe '#execute' do + context 'validation' do + it 'returns error with an invalid branch name' do + result = service.execute('refs/heads/invalid_branch') + + expect(result[:status]).to eq(:error) + expect(result[:message]).to eq('Branch name is invalid') + end + + it 'returns success with a valid branch name' do + result = service.execute('valid_branch_name') + + expect(result[:status]).to eq(:success) + end + end + + context 'branch exist' do + it 'returns error when branch exists' do + result = service.execute('master') + + expect(result[:status]).to eq(:error) + expect(result[:message]).to eq('Branch already exists') + end + + it 'returns success when branch name is available' do + result = service.execute('valid_branch_name') + + expect(result[:status]).to eq(:success) + end + end + end +end diff --git a/spec/services/clusters/kubernetes/create_or_update_namespace_service_spec.rb b/spec/services/clusters/kubernetes/create_or_update_namespace_service_spec.rb index 291e63bbe4a..a9e3e881fd4 100644 --- a/spec/services/clusters/kubernetes/create_or_update_namespace_service_spec.rb +++ b/spec/services/clusters/kubernetes/create_or_update_namespace_service_spec.rb @@ -22,7 +22,7 @@ describe Clusters::Kubernetes::CreateOrUpdateNamespaceService, '#execute' do before do stub_kubeclient_discover(api_url) - stub_kubeclient_get_namespace(api_url) + stub_kubeclient_get_namespaces(api_url) stub_kubeclient_get_service_account_error(api_url, 'gitlab') stub_kubeclient_create_service_account(api_url) stub_kubeclient_get_secret_error(api_url, 'gitlab-token') @@ -39,6 +39,8 @@ describe Clusters::Kubernetes::CreateOrUpdateNamespaceService, '#execute' do stub_kubeclient_put_role_binding(api_url, Clusters::Kubernetes::GITLAB_KNATIVE_SERVING_ROLE_BINDING_NAME, namespace: namespace) stub_kubeclient_put_role(api_url, Clusters::Kubernetes::GITLAB_CROSSPLANE_DATABASE_ROLE_NAME, namespace: namespace) stub_kubeclient_put_role_binding(api_url, Clusters::Kubernetes::GITLAB_CROSSPLANE_DATABASE_ROLE_BINDING_NAME, namespace: namespace) + stub_kubeclient_put_cluster_role(api_url, Clusters::Kubernetes::GITLAB_KNATIVE_VERSION_ROLE_NAME) + stub_kubeclient_put_cluster_role_binding(api_url, Clusters::Kubernetes::GITLAB_KNATIVE_VERSION_ROLE_BINDING_NAME) stub_kubeclient_get_secret( api_url, diff --git a/spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb b/spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb index 4df73fcc2ae..b40861e5aaf 100644 --- a/spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb +++ b/spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb @@ -141,12 +141,15 @@ describe Clusters::Kubernetes::CreateOrUpdateServiceAccountService do before do cluster.platform_kubernetes.rbac! + stub_kubeclient_get_namespaces(api_url) stub_kubeclient_get_role_binding_error(api_url, role_binding_name, namespace: namespace) stub_kubeclient_create_role_binding(api_url, namespace: namespace) stub_kubeclient_put_role(api_url, Clusters::Kubernetes::GITLAB_KNATIVE_SERVING_ROLE_NAME, namespace: namespace) stub_kubeclient_put_role_binding(api_url, Clusters::Kubernetes::GITLAB_KNATIVE_SERVING_ROLE_BINDING_NAME, namespace: namespace) stub_kubeclient_put_role(api_url, Clusters::Kubernetes::GITLAB_CROSSPLANE_DATABASE_ROLE_NAME, namespace: namespace) stub_kubeclient_put_role_binding(api_url, Clusters::Kubernetes::GITLAB_CROSSPLANE_DATABASE_ROLE_BINDING_NAME, namespace: namespace) + stub_kubeclient_put_cluster_role(api_url, Clusters::Kubernetes::GITLAB_KNATIVE_VERSION_ROLE_NAME) + stub_kubeclient_put_cluster_role_binding(api_url, Clusters::Kubernetes::GITLAB_KNATIVE_VERSION_ROLE_BINDING_NAME) end it_behaves_like 'creates service account and token' @@ -234,6 +237,30 @@ describe Clusters::Kubernetes::CreateOrUpdateServiceAccountService do ) ) end + + it 'creates a role and role binding granting the ability to get the version of deployments in knative-serving namespace' do + subject + + expect(WebMock).to have_requested(:put, api_url + "/apis/rbac.authorization.k8s.io/v1/clusterrolebindings/#{Clusters::Kubernetes::GITLAB_KNATIVE_VERSION_ROLE_BINDING_NAME}").with( + body: hash_including( + metadata: { + name: Clusters::Kubernetes::GITLAB_KNATIVE_VERSION_ROLE_BINDING_NAME + }, + roleRef: { + apiGroup: "rbac.authorization.k8s.io", + kind: "ClusterRole", + name: Clusters::Kubernetes::GITLAB_KNATIVE_VERSION_ROLE_NAME + }, + subjects: [ + { + kind: "ServiceAccount", + name: service_account_name, + namespace: namespace + } + ] + ) + ) + end end end end diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb index 1bdea973685..61c8103353c 100644 --- a/spec/services/merge_requests/merge_service_spec.rb +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -211,7 +211,8 @@ describe MergeRequests::MergeService do end it 'does not delete the source branch' do - expect(DeleteBranchService).not_to receive(:new) + expect(::Branches::DeleteService).not_to receive(:new) + service.execute(merge_request) end end @@ -226,7 +227,7 @@ describe MergeRequests::MergeService do end it 'does not delete the source branch' do - expect(DeleteBranchService).not_to receive(:new) + expect(::Branches::DeleteService).not_to receive(:new) service.execute(merge_request) end end @@ -238,7 +239,7 @@ describe MergeRequests::MergeService do end it 'removes the source branch using the author user' do - expect(DeleteBranchService).to receive(:new) + expect(::Branches::DeleteService).to receive(:new) .with(merge_request.source_project, merge_request.author) .and_call_original service.execute(merge_request) @@ -248,7 +249,7 @@ describe MergeRequests::MergeService do let(:service) { described_class.new(project, user, merge_params.merge('should_remove_source_branch' => false)) } it 'does not delete the source branch' do - expect(DeleteBranchService).not_to receive(:new) + expect(::Branches::DeleteService).not_to receive(:new) service.execute(merge_request) end end @@ -260,7 +261,7 @@ describe MergeRequests::MergeService do end it 'removes the source branch using the current user' do - expect(DeleteBranchService).to receive(:new) + expect(::Branches::DeleteService).to receive(:new) .with(merge_request.source_project, user) .and_call_original service.execute(merge_request) diff --git a/spec/services/merge_requests/merge_to_ref_service_spec.rb b/spec/services/merge_requests/merge_to_ref_service_spec.rb index cccafddc450..77e38f1eb4c 100644 --- a/spec/services/merge_requests/merge_to_ref_service_spec.rb +++ b/spec/services/merge_requests/merge_to_ref_service_spec.rb @@ -61,7 +61,7 @@ describe MergeRequests::MergeToRefService do end it 'does not delete the source branch' do - expect(DeleteBranchService).not_to receive(:new) + expect(::Branches::DeleteService).not_to receive(:new) process_merge_to_ref end diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 9d0ad60a624..9e69f179da9 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -113,7 +113,7 @@ describe MergeRequests::RefreshService do context 'when source branch ref does not exists' do before do - DeleteBranchService.new(@project, @user).execute(@merge_request.source_branch) + ::Branches::DeleteService.new(@project, @user).execute(@merge_request.source_branch) end it 'closes MRs without source branch ref' do diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb index 818794ed956..0d7e17ad52c 100644 --- a/spec/services/todo_service_spec.rb +++ b/spec/services/todo_service_spec.rb @@ -1015,6 +1015,30 @@ describe TodoService do end end + describe '#mark_todo_as_done' do + it 'marks a todo done' do + todo1 = create(:todo, :pending, user: john_doe) + + described_class.new.mark_todo_as_done(todo1, john_doe) + + expect(todo1.reload.state).to eq('done') + end + + context 'when todo is already in state done' do + let(:todo1) { create(:todo, :done, user: john_doe) } + + it 'does not update the todo' do + expect { described_class.new.mark_todo_as_done(todo1, john_doe) }.not_to change(todo1.reload, :state) + end + + it 'does not update cache count' do + expect(john_doe).not_to receive(:update_todos_count_cache) + + described_class.new.mark_todo_as_done(todo1, john_doe) + end + end + end + describe '#mark_all_todos_as_done_by_user' do it 'marks all todos done' do todo1 = create(:todo, user: john_doe, state: :pending) diff --git a/spec/support/helpers/kubernetes_helpers.rb b/spec/support/helpers/kubernetes_helpers.rb index 677aef57661..cac43e94a92 100644 --- a/spec/support/helpers/kubernetes_helpers.rb +++ b/spec/support/helpers/kubernetes_helpers.rb @@ -194,6 +194,11 @@ module KubernetesHelpers .to_return(kube_response({})) end + def stub_kubeclient_put_cluster_role_binding(api_url, name) + WebMock.stub_request(:put, api_url + "/apis/rbac.authorization.k8s.io/v1/clusterrolebindings/#{name}") + .to_return(kube_response({})) + end + def stub_kubeclient_get_role_binding(api_url, name, namespace: 'default') WebMock.stub_request(:get, api_url + "/apis/rbac.authorization.k8s.io/v1/namespaces/#{namespace}/rolebindings/#{name}") .to_return(kube_response({})) @@ -219,11 +224,21 @@ module KubernetesHelpers .to_return(kube_response({})) end + def stub_kubeclient_get_namespaces(api_url) + WebMock.stub_request(:get, api_url + '/api/v1/namespaces') + .to_return(kube_response(kube_v1_namespace_list_body)) + end + def stub_kubeclient_get_namespace(api_url, namespace: 'default') WebMock.stub_request(:get, api_url + "/api/v1/namespaces/#{namespace}") .to_return(kube_response({})) end + def stub_kubeclient_put_cluster_role(api_url, name) + WebMock.stub_request(:put, api_url + "/apis/rbac.authorization.k8s.io/v1/clusterroles/#{name}") + .to_return(kube_response({})) + end + def stub_kubeclient_put_role(api_url, name, namespace: 'default') WebMock.stub_request(:put, api_url + "/apis/rbac.authorization.k8s.io/v1/namespaces/#{namespace}/roles/#{name}") .to_return(kube_response({})) @@ -257,6 +272,20 @@ module KubernetesHelpers } end + def kube_v1_namespace_list_body + { + "kind" => "NamespaceList", + "apiVersion" => "v1", + "items" => [ + { + "metadata" => { + "name" => "knative-serving" + } + } + ] + } + end + def kube_v1beta1_discovery_body { "kind" => "APIResourceList", diff --git a/spec/support/helpers/position_tracer_helpers.rb b/spec/support/helpers/position_tracer_helpers.rb index bbf6e06dd40..7516694d4fe 100644 --- a/spec/support/helpers/position_tracer_helpers.rb +++ b/spec/support/helpers/position_tracer_helpers.rb @@ -50,7 +50,7 @@ module PositionTracerHelpers end def create_branch(new_name, branch_name) - CreateBranchService.new(project, current_user).execute(new_name, branch_name) + ::Branches::CreateService.new(project, current_user).execute(new_name, branch_name) end def create_file(branch_name, file_name, content) diff --git a/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb b/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb index c24418b2f90..8962d98218a 100644 --- a/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb +++ b/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb @@ -74,7 +74,7 @@ shared_examples 'handle uploads' do end before do - expect(FileUploader).to receive(:generate_secret).and_return(secret) + allow(FileUploader).to receive(:generate_secret).and_return(secret) UploadService.new(model, jpg, uploader_class).execute end @@ -88,6 +88,18 @@ shared_examples 'handle uploads' do end end + context 'when the upload does not have a MIME type that Rails knows' do + let(:po) { fixture_file_upload('spec/fixtures/missing_metadata.po', 'text/plain') } + + it 'falls back to the null type' do + UploadService.new(model, po, uploader_class).execute + + get :show, params: params.merge(secret: secret, filename: 'missing_metadata.po') + + expect(response.headers['Content-Type']).to eq('application/octet-stream') + end + end + context "when the model is public" do before do model.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PUBLIC) diff --git a/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb b/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb index c0db4cdde72..da966fd2200 100644 --- a/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb +++ b/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb @@ -8,11 +8,14 @@ RSpec.shared_examples 'a creatable merge request' do page.within '.dropdown-menu-user' do click_link user2.name end + expect(find('input[name="merge_request[assignee_ids][]"]', visible: false).value).to match(user2.id.to_s) page.within '.js-assignee-search' do expect(page).to have_content user2.name end + click_link 'Assign to me' + expect(find('input[name="merge_request[assignee_ids][]"]', visible: false).value).to match(user.id.to_s) page.within '.js-assignee-search' do expect(page).to have_content user.name @@ -22,6 +25,7 @@ RSpec.shared_examples 'a creatable merge request' do page.within '.issue-milestone' do click_link milestone.title end + expect(find('input[name="merge_request[milestone_id]"]', visible: false).value).to match(milestone.id.to_s) page.within '.js-milestone-select' do expect(page).to have_content milestone.title @@ -32,6 +36,7 @@ RSpec.shared_examples 'a creatable merge request' do click_link label.title click_link label2.title end + page.within '.js-label-select' do expect(page).to have_content label.title end @@ -58,8 +63,9 @@ RSpec.shared_examples 'a creatable merge request' do it 'updates the branches when selecting a new target project', :js do target_project_member = target_project.owner - CreateBranchService.new(target_project, target_project_member) - .execute('a-brand-new-branch-to-test', 'master') + ::Branches::CreateService.new(target_project, target_project_member) + .execute('a-brand-new-branch-to-test', 'master') + visit project_new_merge_request_path(source_project) first('.js-target-project').click diff --git a/spec/workers/delete_merged_branches_worker_spec.rb b/spec/workers/delete_merged_branches_worker_spec.rb index a218ca921d9..8c983859e36 100644 --- a/spec/workers/delete_merged_branches_worker_spec.rb +++ b/spec/workers/delete_merged_branches_worker_spec.rb @@ -8,8 +8,8 @@ describe DeleteMergedBranchesWorker do let(:project) { create(:project, :repository) } describe "#perform" do - it "calls DeleteMergedBranchesService" do - expect_any_instance_of(DeleteMergedBranchesService).to receive(:execute).and_return(true) + it "delegates to Branches::DeleteMergedService" do + expect_any_instance_of(::Branches::DeleteMergedService).to receive(:execute).and_return(true) worker.perform(project.id, project.owner.id) end |