Commit ce8a0b90 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent dc889678
query mergeRequest($projectPath: ID!, $mergeRequestIID: String!) { query mergeRequest($projectPath: ID!, $mergeRequestIID: ID!) {
project(fullPath: $projectPath) { project(fullPath: $projectPath) {
mergeRequest(iid: $mergeRequestIID) { mergeRequest(iid: $mergeRequestIID) {
createdAt createdAt
......
query ($fullPath: ID!, $iid: String!) { query ($fullPath: ID!, $iid: ID!) {
project (fullPath: $fullPath) { project (fullPath: $fullPath) {
issue (iid: $iid) { issue (iid: $iid) {
iid iid
......
query ($fullPath: ID!, $iid: String!) { query ($fullPath: ID!, $iid: ID!) {
project (fullPath: $fullPath) { project (fullPath: $fullPath) {
issue (iid: $iid) { issue (iid: $iid) {
iid iid
......
...@@ -9,7 +9,7 @@ module Mutations ...@@ -9,7 +9,7 @@ module Mutations
required: true, required: true,
description: "The project the issue to mutate is in" description: "The project the issue to mutate is in"
argument :iid, GraphQL::STRING_TYPE, argument :iid, GraphQL::ID_TYPE,
required: true, required: true,
description: "The iid of the issue to mutate" description: "The iid of the issue to mutate"
......
...@@ -9,7 +9,7 @@ module Mutations ...@@ -9,7 +9,7 @@ module Mutations
required: true, required: true,
description: "The project the merge request to mutate is in" description: "The project the merge request to mutate is in"
argument :iid, GraphQL::STRING_TYPE, argument :iid, GraphQL::ID_TYPE,
required: true, required: true,
description: "The iid of the merge request to mutate" description: "The iid of the merge request to mutate"
......
...@@ -2,11 +2,11 @@ ...@@ -2,11 +2,11 @@
module Resolvers module Resolvers
class IssuesResolver < BaseResolver class IssuesResolver < BaseResolver
argument :iid, GraphQL::STRING_TYPE, argument :iid, GraphQL::ID_TYPE,
required: false, required: false,
description: 'IID of the issue. For example, "1"' description: 'IID of the issue. For example, "1"'
argument :iids, [GraphQL::STRING_TYPE], argument :iids, [GraphQL::ID_TYPE],
required: false, required: false,
description: 'List of IIDs of issues. For example, [1, 2]' description: 'List of IIDs of issues. For example, [1, 2]'
argument :state, Types::IssuableStateEnum, argument :state, Types::IssuableStateEnum,
......
...@@ -2,11 +2,11 @@ ...@@ -2,11 +2,11 @@
module Resolvers module Resolvers
class MergeRequestsResolver < BaseResolver class MergeRequestsResolver < BaseResolver
argument :iid, GraphQL::STRING_TYPE, argument :iid, GraphQL::ID_TYPE,
required: false, required: false,
description: 'The IID of the merge request, e.g., "1"' description: 'The IID of the merge request, e.g., "1"'
argument :iids, [GraphQL::STRING_TYPE], argument :iids, [GraphQL::ID_TYPE],
required: false, required: false,
description: 'The list of IIDs of issues, e.g., [1, 2]' description: 'The list of IIDs of issues, e.g., [1, 2]'
......
...@@ -11,7 +11,7 @@ module Types ...@@ -11,7 +11,7 @@ module Types
field :id, GraphQL::ID_TYPE, null: false, field :id, GraphQL::ID_TYPE, null: false,
description: 'ID of the pipeline' description: 'ID of the pipeline'
field :iid, GraphQL::STRING_TYPE, null: false, field :iid, GraphQL::ID_TYPE, null: false,
description: 'Internal ID of the pipeline' description: 'Internal ID of the pipeline'
field :sha, GraphQL::STRING_TYPE, null: false, field :sha, GraphQL::STRING_TYPE, null: false,
......
...@@ -14,7 +14,7 @@ module Types ...@@ -14,7 +14,7 @@ module Types
field :id, GraphQL::ID_TYPE, null: false, field :id, GraphQL::ID_TYPE, null: false,
description: 'ID of the merge request' description: 'ID of the merge request'
field :iid, GraphQL::STRING_TYPE, null: false, field :iid, GraphQL::ID_TYPE, null: false,
description: 'Internal ID of the merge request' description: 'Internal ID of the merge request'
field :title, GraphQL::STRING_TYPE, null: false, field :title, GraphQL::STRING_TYPE, null: false,
description: 'Title of the merge request' description: 'Title of the merge request'
......
...@@ -31,7 +31,6 @@ module AnalyticsNavbarHelper ...@@ -31,7 +31,6 @@ module AnalyticsNavbarHelper
end end
def cycle_analytics_navbar_link(project, current_user) def cycle_analytics_navbar_link(project, current_user)
return unless Feature.enabled?(:analytics_pages_under_project_analytics_sidebar, project, default_enabled: true)
return unless project_nav_tab?(:cycle_analytics) return unless project_nav_tab?(:cycle_analytics)
navbar_sub_item( navbar_sub_item(
...@@ -43,7 +42,6 @@ module AnalyticsNavbarHelper ...@@ -43,7 +42,6 @@ module AnalyticsNavbarHelper
end end
def repository_analytics_navbar_link(project, current_user) def repository_analytics_navbar_link(project, current_user)
return if Feature.disabled?(:analytics_pages_under_project_analytics_sidebar, project, default_enabled: true)
return if project.empty_repo? return if project.empty_repo?
navbar_sub_item( navbar_sub_item(
...@@ -55,7 +53,6 @@ module AnalyticsNavbarHelper ...@@ -55,7 +53,6 @@ module AnalyticsNavbarHelper
end end
def ci_cd_analytics_navbar_link(project, current_user) def ci_cd_analytics_navbar_link(project, current_user)
return unless Feature.enabled?(:analytics_pages_under_project_analytics_sidebar, project, default_enabled: true)
return unless project_nav_tab?(:pipelines) return unless project_nav_tab?(:pipelines)
return unless project.feature_available?(:builds, current_user) || !project.empty_repo? return unless project.feature_available?(:builds, current_user) || !project.empty_repo?
......
...@@ -130,7 +130,7 @@ class Namespace < ApplicationRecord ...@@ -130,7 +130,7 @@ class Namespace < ApplicationRecord
return unless host.ends_with?(gitlab_host) return unless host.ends_with?(gitlab_host)
name = host.delete_suffix(gitlab_host) name = host.delete_suffix(gitlab_host)
Namespace.find_by_path(name) Namespace.where(parent_id: nil).find_by_path(name)
end end
# overridden in ee # overridden in ee
......
...@@ -164,6 +164,7 @@ class User < ApplicationRecord ...@@ -164,6 +164,7 @@ class User < ApplicationRecord
has_one :status, class_name: 'UserStatus' has_one :status, class_name: 'UserStatus'
has_one :user_preference has_one :user_preference
has_one :user_detail has_one :user_detail
has_one :user_highest_role
# #
# Validations # Validations
......
# frozen_string_literal: true
class UserHighestRole < ApplicationRecord
belongs_to :user, optional: false
validates :highest_access_level, allow_nil: true, inclusion: { in: Gitlab::Access.all_values }
end
...@@ -5,9 +5,8 @@ class AvatarUploader < GitlabUploader ...@@ -5,9 +5,8 @@ class AvatarUploader < GitlabUploader
include RecordsUploads::Concern include RecordsUploads::Concern
include ObjectStorage::Concern include ObjectStorage::Concern
prepend ObjectStorage::Extension::RecordsUploads prepend ObjectStorage::Extension::RecordsUploads
include UploadTypeCheck::Concern
check_upload_type extensions: AvatarUploader::SAFE_IMAGE_EXT MIME_WHITELIST = %w[image/png image/jpeg image/gif image/bmp image/tiff image/vnd.microsoft.icon].freeze
def exists? def exists?
model.avatar.file && model.avatar.file.present? model.avatar.file && model.avatar.file.present?
...@@ -29,6 +28,10 @@ class AvatarUploader < GitlabUploader ...@@ -29,6 +28,10 @@ class AvatarUploader < GitlabUploader
super || 'avatar' super || 'avatar'
end end
def content_type_whitelist
MIME_WHITELIST
end
private private
def dynamic_segment def dynamic_segment
......
# frozen_string_literal: true
# Currently we run CarrierWave 1.3.1 which means we can not whitelist files
# by their content type through magic header parsing.
#
# This is a patch to hold us over until we get to CarrierWave 2 :) It's a mashup of
# CarrierWave's lib/carrierwave/uploader/content_type_whitelist.rb and
# lib/carrierwave/sanitized_file.rb
#
# Include this concern and add a content_type_whitelist method to get the same
# behavior as you would with CarrierWave 2.
#
# This is not an exact replacement as we don't override
# SanitizedFile#content_type but we do set the content_type attribute when we
# check the whitelist.
#
# Remove this after moving to CarrierWave 2, though on practical terms it shouldn't
# break anything if left for a while.
module ContentTypeWhitelist
module Concern
extend ActiveSupport::Concern
private
# CarrierWave calls this method as part of it's before :cache callbacks.
# Here we override and extend CarrierWave's method that does not parse the
# magic headers.
def check_content_type_whitelist!(new_file)
new_file.content_type = mime_magic_content_type(new_file.path)
if content_type_whitelist && !whitelisted_content_type?(new_file.content_type)
message = I18n.translate(:"errors.messages.content_type_whitelist_error", allowed_types: Array(content_type_whitelist).join(", "))
raise CarrierWave::IntegrityError, message
end
super(new_file)
end
def whitelisted_content_type?(content_type)
Array(content_type_whitelist).any? { |item| content_type =~ /#{item}/ }
end
def mime_magic_content_type(path)
if path
File.open(path) do |file|
MimeMagic.by_magic(file).try(:type) || 'invalid/invalid'
end
end
rescue Errno::ENOENT
nil
end
end
end
# frozen_string_literal: true # frozen_string_literal: true
class FaviconUploader < AttachmentUploader class FaviconUploader < AttachmentUploader
include UploadTypeCheck::Concern
EXTENSION_WHITELIST = %w[png ico].freeze EXTENSION_WHITELIST = %w[png ico].freeze
MIME_WHITELIST = %w[image/png image/vnd.microsoft.icon].freeze
check_upload_type extensions: EXTENSION_WHITELIST
def extension_whitelist def extension_whitelist
EXTENSION_WHITELIST EXTENSION_WHITELIST
end end
def content_type_whitelist
MIME_WHITELIST
end
private private
def filename_for_different_format(filename, format) def filename_for_different_format(filename, format)
......
# frozen_string_literal: true # frozen_string_literal: true
class GitlabUploader < CarrierWave::Uploader::Base class GitlabUploader < CarrierWave::Uploader::Base
include ContentTypeWhitelist::Concern
class_attribute :options class_attribute :options
class << self class << self
......
- should_display_analytics_pages_in_sidebar = Feature.enabled?(:analytics_pages_under_group_analytics_sidebar, @group, default_enabled: true)
- issues_count = group_issues_count(state: 'opened') - issues_count = group_issues_count(state: 'opened')
- merge_requests_count = group_merge_requests_count(state: 'opened') - merge_requests_count = group_merge_requests_count(state: 'opened')
...@@ -13,8 +12,7 @@ ...@@ -13,8 +12,7 @@
%ul.sidebar-top-level-items.qa-group-sidebar %ul.sidebar-top-level-items.qa-group-sidebar
- if group_sidebar_link?(:overview) - if group_sidebar_link?(:overview)
- paths = group_overview_nav_link_paths - paths = group_overview_nav_link_paths
- paths << 'contribution_analytics#show' unless should_display_analytics_pages_in_sidebar = nav_link(path: paths, unless: -> { current_path?('groups/contribution_analytics#show') }, html_options: { class: 'home' }) do
= nav_link(path: paths, unless: -> { should_display_analytics_pages_in_sidebar && current_path?('groups/contribution_analytics#show') }, html_options: { class: 'home' }) do
= link_to group_path(@group) do = link_to group_path(@group) do
.nav-icon-container .nav-icon-container
= sprite_icon('home') = sprite_icon('home')
...@@ -45,19 +43,10 @@ ...@@ -45,19 +43,10 @@
%span %span
= _('Activity') = _('Activity')
- unless should_display_analytics_pages_in_sidebar
- if group_sidebar_link?(:contribution_analytics)
= nav_link(path: 'contribution_analytics#show') do
= link_to group_contribution_analytics_path(@group), title: _('Contribution'), data: { placement: 'right', qa_selector: 'contribution_analytics_link' } do
%span
= _('Contribution')
= render_if_exists 'layouts/nav/group_insights_link'
= render_if_exists "layouts/nav/ee/epic_link", group: @group = render_if_exists "layouts/nav/ee/epic_link", group: @group
- if group_sidebar_link?(:issues) - if group_sidebar_link?(:issues)
= nav_link(path: group_issues_sub_menu_items, unless: -> { should_display_analytics_pages_in_sidebar && current_path?('issues_analytics#show') }) do = nav_link(path: group_issues_sub_menu_items, unless: -> { current_path?('issues_analytics#show') }) do
= link_to issues_group_path(@group), data: { qa_selector: 'group_issues_item' } do = link_to issues_group_path(@group), data: { qa_selector: 'group_issues_item' } do
.nav-icon-container .nav-icon-container
= sprite_icon('issues') = sprite_icon('issues')
...@@ -84,9 +73,6 @@ ...@@ -84,9 +73,6 @@
%span %span
= boards_link_text = boards_link_text
- unless should_display_analytics_pages_in_sidebar
= render_if_exists 'layouts/nav/issues_analytics_link'
- if group_sidebar_link?(:labels) - if group_sidebar_link?(:labels)
= nav_link(path: 'labels#index') do = nav_link(path: 'labels#index') do
= link_to group_labels_path(@group), title: _('Labels') do = link_to group_labels_path(@group), title: _('Labels') do
......
- should_display_analytics_pages_in_sidebar = Feature.enabled?(:analytics_pages_under_project_analytics_sidebar, @project, default_enabled: true)
.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?) } .nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?) }
.nav-sidebar-inner-scroll .nav-sidebar-inner-scroll
- can_edit = can?(current_user, :admin_project, @project) - can_edit = can?(current_user, :admin_project, @project)
...@@ -10,9 +8,7 @@ ...@@ -10,9 +8,7 @@
.sidebar-context-title .sidebar-context-title
= @project.name = @project.name
%ul.sidebar-top-level-items.qa-project-sidebar %ul.sidebar-top-level-items.qa-project-sidebar
- paths = sidebar_projects_paths = nav_link(path: sidebar_projects_paths, html_options: { class: 'home' }) do
- paths << 'cycle_analytics#show' unless should_display_analytics_pages_in_sidebar
= nav_link(path: paths, html_options: { class: 'home' }) do
= link_to project_path(@project), class: 'shortcuts-project rspec-project-link', data: { qa_selector: 'project_link' } do = link_to project_path(@project), class: 'shortcuts-project rspec-project-link', data: { qa_selector: 'project_link' } do
.nav-icon-container .nav-icon-container
= sprite_icon('home') = sprite_icon('home')
...@@ -39,17 +35,8 @@ ...@@ -39,17 +35,8 @@
%span= _('Releases') %span= _('Releases')
- unless should_display_analytics_pages_in_sidebar
- if can?(current_user, :read_cycle_analytics, @project)
= nav_link(path: 'cycle_analytics#show') do
= link_to project_cycle_analytics_path(@project), title: _('Value Stream'), class: 'shortcuts-project-cycle-analytics' do
%span= _('Value Stream')
= render_if_exists 'layouts/nav/project_insights_link'
- if project_nav_tab? :files - if project_nav_tab? :files
= nav_link(controller: sidebar_repository_paths, unless: -> { should_display_analytics_pages_in_sidebar && current_path?('projects/graphs#charts') }) do = nav_link(controller: sidebar_repository_paths, unless: -> { current_path?('projects/graphs#charts') }) do
= link_to project_tree_path(@project), class: 'shortcuts-tree qa-project-menu-repo' do = link_to project_tree_path(@project), class: 'shortcuts-tree qa-project-menu-repo' do
.nav-icon-container .nav-icon-container
= sprite_icon('doc-text') = sprite_icon('doc-text')
...@@ -90,11 +77,6 @@ ...@@ -90,11 +77,6 @@
= link_to project_compare_index_path(@project, from: @repository.root_ref, to: current_ref) do = link_to project_compare_index_path(@project, from: @repository.root_ref, to: current_ref) do
= _('Compare') = _('Compare')
- unless should_display_analytics_pages_in_sidebar
= nav_link(path: 'graphs#charts') do
= link_to charts_project_graph_path(@project, current_ref) do
= _('Charts')
= render_if_exists 'projects/sidebar/repository_locked_files' = render_if_exists 'projects/sidebar/repository_locked_files'
- if project_nav_tab? :issues - if project_nav_tab? :issues
...@@ -178,7 +160,7 @@ ...@@ -178,7 +160,7 @@
= number_with_delimiter(@project.open_merge_requests_count) = number_with_delimiter(@project.open_merge_requests_count)
- if project_nav_tab? :pipelines - if project_nav_tab? :pipelines
= nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts], unless: -> { should_display_analytics_pages_in_sidebar && current_path?('projects/pipelines#charts') }) do = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts], unless: -> { current_path?('projects/pipelines#charts') }) do
= link_to project_pipelines_path(@project), class: 'shortcuts-pipelines qa-link-pipelines rspec-link-pipelines', data: { qa_selector: 'ci_cd_link' } do = link_to project_pipelines_path(@project), class: 'shortcuts-pipelines qa-link-pipelines rspec-link-pipelines', data: { qa_selector: 'ci_cd_link' } do
.nav-icon-container .nav-icon-container
= sprite_icon('rocket') = sprite_icon('rocket')
...@@ -215,12 +197,6 @@ ...@@ -215,12 +197,6 @@
%span %span
= _('Schedules') = _('Schedules')
- if !should_display_analytics_pages_in_sidebar && @project.feature_available?(:builds, current_user) && !@project.empty_repo?
= nav_link(path: 'pipelines#charts') do
= link_to charts_project_pipelines_path(@project), title: _('Charts'), class: 'shortcuts-pipelines-charts' do
%span
= _('Charts')
= render_if_exists 'layouts/nav/sidebar/project_security_link' # EE-specific = render_if_exists 'layouts/nav/sidebar/project_security_link' # EE-specific
- if project_nav_tab? :operations - if project_nav_tab? :operations
...@@ -426,13 +402,6 @@ ...@@ -426,13 +402,6 @@
= link_to project_network_path(@project, current_ref), title: _('Network'), class: 'shortcuts-network' do = link_to project_network_path(@project, current_ref), title: _('Network'), class: 'shortcuts-network' do
= _('Graph') = _('Graph')
- unless should_display_analytics_pages_in_sidebar
-# Shortcut to Repository > Charts (formerly, top-nav item "Graphs")
- unless @project.empty_repo?
%li.hidden
= link_to charts_project_graph_path(@project, current_ref), title: _('Charts'), class: 'shortcuts-repository-charts' do
= _('Charts')
-# Shortcut to Issues > New Issue -# Shortcut to Issues > New Issue
- if project_nav_tab?(:issues) - if project_nav_tab?(:issues)
%li.hidden %li.hidden
......
This diff is collapsed.
...@@ -8,7 +8,7 @@ module WorkerAttributes ...@@ -8,7 +8,7 @@ module WorkerAttributes
VALID_RESOURCE_BOUNDARIES = [:memory, :cpu, :unknown].freeze VALID_RESOURCE_BOUNDARIES = [:memory, :cpu, :unknown].freeze
# Urgencies that workers can declare through the `urgencies` attribute # Urgencies that workers can declare through the `urgencies` attribute
VALID_URGENCIES = [:high, :default, :none].freeze VALID_URGENCIES = [:high, :low, :throttled].freeze
NAMESPACE_WEIGHTS = { NAMESPACE_WEIGHTS = {
auto_devops: 2, auto_devops: 2,
...@@ -65,7 +65,7 @@ module WorkerAttributes ...@@ -65,7 +65,7 @@ module WorkerAttributes
end end
def get_urgency def get_urgency
worker_attributes[:urgency] || :default worker_attributes[:urgency] || :low
end end
# Set this attribute on a job when it will call to services outside of the # Set this attribute on a job when it will call to services outside of the
......
...@@ -26,8 +26,8 @@ class ErrorTrackingIssueLinkWorker # rubocop:disable Scalability/IdempotentWorke ...@@ -26,8 +26,8 @@ class ErrorTrackingIssueLinkWorker # rubocop:disable Scalability/IdempotentWorke
logger.info("Linking Sentry issue #{sentry_issue_id} to GitLab issue #{issue.id}") logger.info("Linking Sentry issue #{sentry_issue_id} to GitLab issue #{issue.id}")
sentry_client.create_issue_link(integration_id, sentry_issue_id, issue) sentry_client.create_issue_link(integration_id, sentry_issue_id, issue)
rescue Sentry::Client::Error rescue Sentry::Client::Error => e
logger.info("Failed to link Sentry issue #{sentry_issue_id} to GitLab issue #{issue.id}") logger.info("Failed to link Sentry issue #{sentry_issue_id} to GitLab issue #{issue.id} with error: #{e.message}")
end end
end end
......
...@@ -18,21 +18,7 @@ class UpdateMergeRequestsWorker # rubocop:disable Scalability/IdempotentWorker ...@@ -18,21 +18,7 @@ class UpdateMergeRequestsWorker # rubocop:disable Scalability/IdempotentWorker
user = User.find_by(id: user_id) user = User.find_by(id: user_id)
return unless user return unless user
# TODO: remove this benchmarking when we have rich logging
time = Benchmark.measure do
MergeRequests::RefreshService.new(project, user).execute(oldrev, newrev, ref) MergeRequests::RefreshService.new(project, user).execute(oldrev, newrev, ref)
end end
args_log = [
"elapsed=#{time.real}",
"project_id=#{project_id}",
"user_id=#{user_id}",
"oldrev=#{oldrev}",
"newrev=#{newrev}",
"ref=#{ref}"
].join(',')
Rails.logger.info("UpdateMergeRequestsWorker#perform #{args_log}") if time.real > LOG_TIME_THRESHOLD # rubocop:disable Gitlab/RailsLogger
end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
end end
---
title: Replace avatar and favicon upload type consistency validation with content whitelist validation
merge_request: 25401
author:
type: changed
---
title: Introduce database table for user highest roles
merge_request: 26987
author:
type: added
...@@ -6,8 +6,8 @@ en: ...@@ -6,8 +6,8 @@ en:
carrierwave_download_error: could not be downloaded carrierwave_download_error: could not be downloaded
extension_whitelist_error: "You are not allowed to upload %{extension} files, allowed types: %{allowed_types}" extension_whitelist_error: "You are not allowed to upload %{extension} files, allowed types: %{allowed_types}"
extension_blacklist_error: "You are not allowed to upload %{extension} files, prohibited types: %{prohibited_types}" extension_blacklist_error: "You are not allowed to upload %{extension} files, prohibited types: %{prohibited_types}"
content_type_whitelist_error: "You are not allowed to upload %{content_type} files" content_type_whitelist_error: "file format is not supported. Please try one of the following supported formats: %{allowed_types}"
content_type_blacklist_error: "You are not allowed to upload %{content_type} files" content_type_blacklist_error: "You are not allowed to upload %{content_type} files, prohibited types: %{allowed_types}"
rmagick_processing_error: "Failed to manipulate with rmagick, maybe it is not an image?" rmagick_processing_error: "Failed to manipulate with rmagick, maybe it is not an image?"
mini_magick_processing_error: "Failed to manipulate with MiniMagick, maybe it is not an image? Original Error: %{e}" mini_magick_processing_error: "Failed to manipulate with MiniMagick, maybe it is not an image? Original Error: %{e}"
min_size_error: "File size should be greater than %{min_size}" min_size_error: "File size should be greater than %{min_size}"
......
# frozen_string_literal: true
class CreateUserHighestRoles < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
with_lock_retries do
create_table :user_highest_roles, id: false do |t|
t.datetime_with_timezone :updated_at, null: false
t.references :user, primary_key: true, default: nil, index: false, foreign_key: { on_delete: :cascade }
t.integer :highest_access_level
t.index [:user_id, :highest_access_level]
end
end
end
def down
with_lock_retries do
drop_table :user_highest_roles
end
end
end
...@@ -4251,6 +4251,12 @@ ActiveRecord::Schema.define(version: 2020_03_11_165635) do ...@@ -4251,6 +4251,12 @@ ActiveRecord::Schema.define(version: 2020_03_11_165635) do
t.index ["user_id"], name: "index_user_details_on_user_id", unique: true t.index ["user_id"], name: "index_user_details_on_user_id", unique: true
end end
create_table "user_highest_roles", primary_key: "user_id", id: :bigint, default: nil, force: :cascade do |t|
t.datetime_with_timezone "updated_at", null: false
t.integer "highest_access_level"
t.index ["user_id", "highest_access_level"], name: "index_user_highest_roles_on_user_id_and_highest_access_level"
end
create_table "user_interacted_projects", id: false, force: :cascade do |t| create_table "user_interacted_projects", id: false, force: :cascade do |t|
t.integer "user_id", null: false t.integer "user_id", null: false
t.integer "project_id", null: false t.integer "project_id", null: false
...@@ -5135,6 +5141,7 @@ ActiveRecord::Schema.define(version: 2020_03_11_165635) do ...@@ -5135,6 +5141,7 @@ ActiveRecord::Schema.define(version: 2020_03_11_165635) do
add_foreign_key "user_callouts", "users", on_delete: :cascade add_foreign_key "user_callouts", "users", on_delete: :cascade
add_foreign_key "user_custom_attributes", "users", on_delete: :cascade add_foreign_key "user_custom_attributes", "users", on_delete: :cascade
add_foreign_key "user_details", "users", on_delete: :cascade add_foreign_key "user_details", "users", on_delete: :cascade
add_foreign_key "user_highest_roles", "users", on_delete: :cascade
add_foreign_key "user_interacted_projects", "projects", name: "fk_722ceba4f7", on_delete: :cascade add_foreign_key "user_interacted_projects", "projects", name: "fk_722ceba4f7", on_delete: :cascade
add_foreign_key "user_interacted_projects", "users", name: "fk_0894651f08", on_delete: :cascade add_foreign_key "user_interacted_projects", "users", name: "fk_0894651f08", on_delete: :cascade
add_foreign_key "user_preferences", "users", on_delete: :cascade add_foreign_key "user_preferences", "users", on_delete: :cascade
......
...@@ -30,7 +30,7 @@ This configuration is supported in [GitLab Starter, Premium and Ultimate](https: ...@@ -30,7 +30,7 @@ This configuration is supported in [GitLab Starter, Premium and Ultimate](https:
References: References:
- [Installation Docs](../../install/README.html) - [Installation Docs](../../install/README.md)
- [Backup/Restore Docs](https://docs.gitlab.com/omnibus/settings/backups.html#backup-and-restore-omnibus-gitlab-configuration) - [Backup/Restore Docs](https://docs.gitlab.com/omnibus/settings/backups.html#backup-and-restore-omnibus-gitlab-configuration)
### Level 2: Multiple application servers ### Level 2: Multiple application servers
...@@ -68,7 +68,7 @@ This configuration is supported in [GitLab Premium and Ultimate](https://about.g ...@@ -68,7 +68,7 @@ This configuration is supported in [GitLab Premium and Ultimate](https://about.g
References: References:
- [Geo Documentation](../../gitlab-geo/README.html) - [Geo Documentation](../../gitlab-geo/README.html)
- [GitLab Geo with a highly available configuration](../geo/replication/high_availability.html) - [GitLab Geo with a highly available configuration](../geo/replication/high_availability.md)
## Recommended setups based on number of users ## Recommended setups based on number of users
......
...@@ -130,7 +130,7 @@ following attributes: ...@@ -130,7 +130,7 @@ following attributes:
- `has_external_dependencies` - whether or not the queue connects to external - `has_external_dependencies` - whether or not the queue connects to external
services. For example, all importers have this set to `true`. services. For example, all importers have this set to `true`.
- `urgency` - how important it is that this queue's jobs run - `urgency` - how important it is that this queue's jobs run
quickly. Can be `high`, `default`, or `none`. For example, the quickly. Can be `high`, `low`, or `throttled`. For example, the
`authorized_projects` queue is used to refresh user permissions, and `authorized_projects` queue is used to refresh user permissions, and
is high urgency. is high urgency.
- `name` - the queue name. The other attributes are typically more useful as - `name` - the queue name. The other attributes are typically more useful as
......
...@@ -3757,7 +3757,7 @@ input IssueSetConfidentialInput { ...@@ -3757,7 +3757,7 @@ input IssueSetConfidentialInput {
""" """
The iid of the issue to mutate The iid of the issue to mutate
""" """
iid: String! iid: ID!
""" """
The project the issue to mutate is in The project the issue to mutate is in
...@@ -3802,7 +3802,7 @@ input IssueSetDueDateInput { ...@@ -3802,7 +3802,7 @@ input IssueSetDueDateInput {
""" """
The iid of the issue to mutate The iid of the issue to mutate
""" """
iid: String! iid: ID!
""" """
The project the issue to mutate is in The project the issue to mutate is in
...@@ -3842,7 +3842,7 @@ input IssueSetWeightInput { ...@@ -3842,7 +3842,7 @@ input IssueSetWeightInput {
""" """
The iid of the issue to mutate The iid of the issue to mutate
""" """
iid: String! iid: ID!
""" """
The project the issue to mutate is in The project the issue to mutate is in
...@@ -4150,7 +4150,7 @@ type MergeRequest implements Noteable { ...@@ -4150,7 +4150,7 @@ type MergeRequest implements Noteable {
""" """
Internal ID of the merge request Internal ID of the merge request
""" """
iid: String! iid: ID!
""" """
Commit SHA of the merge request if merge is in progress Commit SHA of the merge request if merge is in progress
...@@ -4550,7 +4550,7 @@ input MergeRequestSetAssigneesInput { ...@@ -4550,7 +4550,7 @@ input MergeRequestSetAssigneesInput {
""" """
The iid of the merge request to mutate The iid of the merge request to mutate
""" """
iid: String! iid: ID!
""" """
The operation to perform. Defaults to REPLACE. The operation to perform. Defaults to REPLACE.
...@@ -4595,7 +4595,7 @@ input MergeRequestSetLabelsInput { ...@@ -4595,7 +4595,7 @@ input MergeRequestSetLabelsInput {
""" """
The iid of the merge request to mutate The iid of the merge request to mutate
""" """
iid: String! iid: ID!
""" """
The Label IDs to set. Replaces existing labels by default. The Label IDs to set. Replaces existing labels by default.
...@@ -4645,7 +4645,7 @@ input MergeRequestSetLockedInput { ...@@ -4645,7 +4645,7 @@ input MergeRequestSetLockedInput {
""" """
The iid of the merge request to mutate The iid of the merge request to mutate
""" """
iid: String! iid: ID!
""" """
Whether or not to lock the merge request. Whether or not to lock the merge request.
...@@ -4690,7 +4690,7 @@ input MergeRequestSetMilestoneInput { ...@@ -4690,7 +4690,7 @@ input MergeRequestSetMilestoneInput {
""" """
The iid of the merge request to mutate The iid of the merge request to mutate
""" """
iid: String! iid: ID!
""" """
The milestone to assign to the merge request. The milestone to assign to the merge request.
...@@ -4735,7 +4735,7 @@ input MergeRequestSetSubscriptionInput { ...@@ -4735,7 +4735,7 @@ input MergeRequestSetSubscriptionInput {
""" """
The iid of the merge request to mutate The iid of the merge request to mutate
""" """
iid: String! iid: ID!
""" """
The project the merge request to mutate is in The project the merge request to mutate is in
...@@ -4780,7 +4780,7 @@ input MergeRequestSetWipInput { ...@@ -4780,7 +4780,7 @@ input MergeRequestSetWipInput {
""" """
The iid of the merge request to mutate The iid of the merge request to mutate
""" """
iid: String! iid: ID!
""" """
The project the merge request to mutate is in The project the merge request to mutate is in
...@@ -5351,7 +5351,7 @@ type Pipeline { ...@@ -5351,7 +5351,7 @@ type Pipeline {
""" """
Internal ID of the pipeline Internal ID of the pipeline
""" """
iid: String! iid: ID!
""" """
SHA of the pipeline's commit SHA of the pipeline's commit
...@@ -5629,12 +5629,12 @@ type Project { ...@@ -5629,12 +5629,12 @@ type Project {
""" """
IID of the issue. For example, "1" IID of the issue. For example, "1"
""" """
iid: String iid: ID
""" """
List of IIDs of issues. For example, [1, 2] List of IIDs of issues. For example, [1, 2]
""" """
iids: [String!] iids: [ID!]
""" """
Labels applied to this issue Labels applied to this issue
...@@ -5724,12 +5724,12 @@ type Project { ...@@ -5724,12 +5724,12 @@ type Project {
""" """
IID of the issue. For example, "1" IID of the issue. For example, "1"
""" """
iid: String iid: ID
""" """
List of IIDs of issues. For example, [1, 2] List of IIDs of issues. For example, [1, 2]
""" """
iids: [String!] iids: [ID!]
""" """
Labels applied to this issue Labels applied to this issue
...@@ -5799,12 +5799,12 @@ type Project { ...@@ -5799,12 +5799,12 @@ type Project {
""" """
The IID of the merge request, e.g., "1" The IID of the merge request, e.g., "1"
""" """
iid: String iid: ID
""" """
The list of IIDs of issues, e.g., [1, 2] The list of IIDs of issues, e.g., [1, 2]
""" """
iids: [String!] iids: [ID!]
): MergeRequest ): MergeRequest
""" """
...@@ -5829,12 +5829,12 @@ type Project { ...@@ -5829,12 +5829,12 @@ type Project {
""" """
The IID of the merge request, e.g., "1" The IID of the merge request, e.g., "1"
""" """
iid: String iid: ID
""" """
The list of IIDs of issues, e.g., [1, 2] The list of IIDs of issues, e.g., [1, 2]
""" """
iids: [String!] iids: [ID!]
""" """
Returns the last _n_ elements from the list. Returns the last _n_ elements from the list.
...@@ -8112,7 +8112,7 @@ input UpdateIssueInput { ...@@ -8112,7 +8112,7 @@ input UpdateIssueInput {
""" """
The iid of the issue to mutate The iid of the issue to mutate
""" """
iid: String! iid: ID!
""" """
The project the issue to mutate is in The project the issue to mutate is in
......
...@@ -702,7 +702,7 @@ ...@@ -702,7 +702,7 @@
"description": "IID of the issue. For example, \"1\"", "description": "IID of the issue. For example, \"1\"",
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "String", "name": "ID",
"ofType": null "ofType": null
}, },
"defaultValue": null "defaultValue": null
...@@ -718,7 +718,7 @@ ...@@ -718,7 +718,7 @@
"name": null, "name": null,
"ofType": { "ofType": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "String", "name": "ID",
"ofType": null "ofType": null
} }
} }
...@@ -881,7 +881,7 @@ ...@@ -881,7 +881,7 @@
"description": "IID of the issue. For example, \"1\"", "description": "IID of the issue. For example, \"1\"",
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "String", "name": "ID",
"ofType": null "ofType": null
}, },
"defaultValue": null "defaultValue": null
...@@ -897,7 +897,7 @@ ...@@ -897,7 +897,7 @@
"name": null, "name": null,
"ofType": { "ofType": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "String", "name": "ID",
"ofType": null "ofType": null
} }
} }
...@@ -1156,7 +1156,7 @@ ...@@ -1156,7 +1156,7 @@
"description": "The IID of the merge request, e.g., \"1\"", "description": "The IID of the merge request, e.g., \"1\"",
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "String", "name": "ID",
"ofType": null "ofType": null
}, },
"defaultValue": null "defaultValue": null
...@@ -1172,7 +1172,7 @@ ...@@ -1172,7 +1172,7 @@
"name": null, "name": null,
"ofType": { "ofType": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "String", "name": "ID",
"ofType": null "ofType": null
} }
} }
...@@ -1197,7 +1197,7 @@ ...@@ -1197,7 +1197,7 @@
"description": "The IID of the merge request, e.g., \"1\"", "description": "The IID of the merge request, e.g., \"1\"",
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "String", "name": "ID",
"ofType": null "ofType": null
}, },
"defaultValue": null "defaultValue": null
...@@ -1213,7 +1213,7 @@ ...@@ -1213,7 +1213,7 @@
"name": null, "name": null,
"ofType": { "ofType": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "String", "name": "ID",
"ofType": null "ofType": null
} }
} }
...@@ -14425,7 +14425,7 @@ ...@@ -14425,7 +14425,7 @@
"name": null, "name": null,
"ofType": { "ofType": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "String", "name": "ID",
"ofType": null "ofType": null
} }
}, },
...@@ -16204,7 +16204,7 @@ ...@@ -16204,7 +16204,7 @@
"name": null, "name": null,
"ofType": { "ofType": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "String", "name": "ID",
"ofType": null "ofType": null
} }
}, },
...@@ -20999,7 +20999,7 @@ ...@@ -20999,7 +20999,7 @@
"name": null, "name": null,
"ofType": { "ofType": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "String", "name": "ID",
"ofType": null "ofType": null
} }
}, },
...@@ -21129,7 +21129,7 @@ ...@@ -21129,7 +21129,7 @@
"name": null, "name": null,
"ofType": { "ofType": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "String", "name": "ID",
"ofType": null "ofType": null
} }
}, },
...@@ -21259,7 +21259,7 @@ ...@@ -21259,7 +21259,7 @@
"name": null, "name": null,
"ofType": { "ofType": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "String", "name": "ID",
"ofType": null "ofType": null
} }
}, },
...@@ -21425,7 +21425,7 @@ ...@@ -21425,7 +21425,7 @@
"name": null, "name": null,
"ofType": { "ofType": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "String", "name": "ID",
"ofType": null "ofType": null
} }
}, },
...@@ -21602,7 +21602,7 @@ ...@@ -21602,7 +21602,7 @@
"name": null, "name": null,
"ofType": { "ofType": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "String", "name": "ID",
"ofType": null "ofType": null
} }
}, },
...@@ -21732,7 +21732,7 @@ ...@@ -21732,7 +21732,7 @@
"name": null, "name": null,
"ofType": { "ofType": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "String", "name": "ID",
"ofType": null "ofType": null
} }
}, },
...@@ -21858,7 +21858,7 @@ ...@@ -21858,7 +21858,7 @@
"name": null, "name": null,
"ofType": { "ofType": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "String", "name": "ID",
"ofType": null "ofType": null
} }
}, },
...@@ -21988,7 +21988,7 @@ ...@@ -21988,7 +21988,7 @@
"name": null, "name": null,
"ofType": { "ofType": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "String", "name": "ID",
"ofType": null "ofType": null
} }
}, },
...@@ -22118,7 +22118,7 @@ ...@@ -22118,7 +22118,7 @@
"name": null, "name": null,
"ofType": { "ofType": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "String", "name": "ID",
"ofType": null "ofType": null
} }
}, },
...@@ -24554,7 +24554,7 @@ ...@@ -24554,7 +24554,7 @@
"name": null, "name": null,
"ofType": { "ofType": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "String", "name": "ID",
"ofType": null "ofType": null
} }
}, },
......
...@@ -630,7 +630,7 @@ Autogenerated return type of MarkAsSpamSnippet ...@@ -630,7 +630,7 @@ Autogenerated return type of MarkAsSpamSnippet
| `forceRemoveSourceBranch` | Boolean | Indicates if the project settings will lead to source branch deletion after merge | | `forceRemoveSourceBranch` | Boolean | Indicates if the project settings will lead to source branch deletion after merge |
| `headPipeline` | Pipeline | The pipeline running on the branch HEAD of the merge request | | `headPipeline` | Pipeline | The pipeline running on the branch HEAD of the merge request |
| `id` | ID! | ID of the merge request | | `id` | ID! | ID of the merge request |
| `iid` | String! | Internal ID of the merge request | | `iid` | ID! | Internal ID of the merge request |
| `inProgressMergeCommitSha` | String | Commit SHA of the merge request if merge is in progress | | `inProgressMergeCommitSha` | String | Commit SHA of the merge request if merge is in progress |
| `mergeCommitMessage` **{warning-solid}** | String | **Deprecated:** Renamed to defaultMergeCommitMessage | | `mergeCommitMessage` **{warning-solid}** | String | **Deprecated:** Renamed to defaultMergeCommitMessage |
| `mergeCommitSha` | String | SHA of the merge request commit (set once merged) | | `mergeCommitSha` | String | SHA of the merge request commit (set once merged) |
...@@ -834,7 +834,7 @@ Information about pagination in a connection. ...@@ -834,7 +834,7 @@ Information about pagination in a connection.
| `duration` | Int | Duration of the pipeline in seconds | | `duration` | Int | Duration of the pipeline in seconds |
| `finishedAt` | Time | Timestamp of the pipeline's completion | | `finishedAt` | Time | Timestamp of the pipeline's completion |
| `id` | ID! | ID of the pipeline | | `id` | ID! | ID of the pipeline |
| `iid` | String! | Internal ID of the pipeline | | `iid` | ID! | Internal ID of the pipeline |
| `sha` | String! | SHA of the pipeline's commit | | `sha` | String! | SHA of the pipeline's commit |
| `startedAt` | Time | Timestamp when the pipeline was started | | `startedAt` | Time | Timestamp when the pipeline was started |
| `status` | PipelineStatusEnum! | Status of the pipeline (CREATED, WAITING_FOR_RESOURCE, PREPARING, PENDING, RUNNING, FAILED, SUCCESS, CANCELED, SKIPPED, MANUAL, SCHEDULED) | | `status` | PipelineStatusEnum! | Status of the pipeline (CREATED, WAITING_FOR_RESOURCE, PREPARING, PENDING, RUNNING, FAILED, SUCCESS, CANCELED, SKIPPED, MANUAL, SCHEDULED) |
......
...@@ -541,7 +541,7 @@ argument :project_path, GraphQL::ID_TYPE, ...@@ -541,7 +541,7 @@ argument :project_path, GraphQL::ID_TYPE,
required: true, required: true,
description: "The project the merge request to mutate is in" description: "The project the merge request to mutate is in"
argument :iid, GraphQL::STRING_TYPE, argument :iid, GraphQL::ID_TYPE,
required: true, required: true,
description: "The iid of the merge request to mutate" description: "The iid of the merge request to mutate"
......
...@@ -124,13 +124,13 @@ Consider skipping the cop if you're not confident your job can safely run multip ...@@ -124,13 +124,13 @@ Consider skipping the cop if you're not confident your job can safely run multip
## Job urgency ## Job urgency
Jobs can have an `urgency` attribute set, which can be `:high`, Jobs can have an `urgency` attribute set, which can be `:high`,
`:default`, or `:none`. These have the below targets: `:low`, or `:throttled`. These have the below targets:
| **Urgency** | **Queue Scheduling Target** | **Execution Latency Requirement** | | **Urgency** | **Queue Scheduling Target** | **Execution Latency Requirement** |
|-------------|-----------------------------|------------------------------------| |--------------|-----------------------------|------------------------------------|
| `:high` | 100 milliseconds | p50 of 1 second, p99 of 10 seconds | | `:high` | 100 milliseconds | p50 of 1 second, p99 of 10 seconds |
| `:default` | 1 minute | Maximum run time of 1 hour | | `:low` | 1 minute | Maximum run time of 1 hour |
| `:none` | None | Maximum run time of 1 hour | | `:throttled` | None | Maximum run time of 1 hour |
To set a job's urgency, use the `urgency` class method: To set a job's urgency, use the `urgency` class method:
...@@ -175,7 +175,7 @@ these jobs also have very strict execution duration requirements: ...@@ -175,7 +175,7 @@ these jobs also have very strict execution duration requirements:
If a worker cannot meet these expectations, then it cannot be treated as a If a worker cannot meet these expectations, then it cannot be treated as a
`urgency :high` worker: consider redesigning the worker, or splitting the `urgency :high` worker: consider redesigning the worker, or splitting the
work between two different workers, one with `urgency :high` code that work between two different workers, one with `urgency :high` code that
executes quickly, and the other with `urgency :default`, which has no executes quickly, and the other with `urgency :low`, which has no
execution latency requirements (but also has lower scheduling targets). execution latency requirements (but also has lower scheduling targets).
## Jobs with External Dependencies ## Jobs with External Dependencies
......
...@@ -6,6 +6,7 @@ module Gitlab ...@@ -6,6 +6,7 @@ module Gitlab
module Test module Test
class Junit class Junit
JunitParserError = Class.new(Gitlab::Ci::Parsers::ParserError) JunitParserError = Class.new(Gitlab::Ci::Parsers::ParserError)
ATTACHMENT_TAG_REGEX = /\[\[ATTACHMENT\|(?<path>.+?)\]\]/.freeze
def parse!(xml_data, test_suite) def parse!(xml_data, test_suite)
root = Hash.from_xml(xml_data) root = Hash.from_xml(xml_data)
...@@ -49,6 +50,7 @@ module Gitlab ...@@ -49,6 +50,7 @@ module Gitlab
if data['failure'] if data['failure']
status = ::Gitlab::Ci::Reports::TestCase::STATUS_FAILED status = ::Gitlab::Ci::Reports::TestCase::STATUS_FAILED
system_output = data['failure'] system_output = data['failure']
attachment = attachment_path(data['system_out'])
elsif data['error'] elsif data['error']
status = ::Gitlab::Ci::Reports::TestCase::STATUS_ERROR status = ::Gitlab::Ci::Reports::TestCase::STATUS_ERROR
system_output = data['error'] system_output = data['error']
...@@ -63,9 +65,17 @@ module Gitlab ...@@ -63,9 +65,17 @@ module Gitlab
file: data['file'], file: data['file'],
execution_time: data['time'], execution_time: data['time'],
status: status, status: status,
system_output: system_output system_output: system_output,
attachment: attachment
) )
end end
def attachment_path(data)
return unless data
matches = data.match(ATTACHMENT_TAG_REGEX)
matches[:path] if matches
end
end end
end end
end end
......
...@@ -10,9 +10,9 @@ module Gitlab ...@@ -10,9 +10,9 @@ module Gitlab
STATUS_ERROR = 'error' STATUS_ERROR = 'error'
STATUS_TYPES = [STATUS_SUCCESS, STATUS_FAILED, STATUS_SKIPPED, STATUS_ERROR].freeze STATUS_TYPES = [STATUS_SUCCESS, STATUS_FAILED, STATUS_SKIPPED, STATUS_ERROR].freeze
attr_reader :name, :classname, :execution_time, :status, :file, :system_output, :stack_trace, :key attr_reader :name, :classname, :execution_time, :status, :file, :system_output, :stack_trace, :key, :attachment
def initialize(name:, classname:, execution_time:, status:, file: nil, system_output: nil, stack_trace: nil) def initialize(name:, classname:, execution_time:, status:, file: nil, system_output: nil, stack_trace: nil, attachment: nil)
@name = name @name = name
@classname = classname @classname = classname
@file = file @file = file
...@@ -21,6 +21,11 @@ module Gitlab ...@@ -21,6 +21,11 @@ module Gitlab
@system_output = system_output @system_output = system_output
@stack_trace = stack_trace @stack_trace = stack_trace
@key = sanitize_key_name("#{classname}_#{name}") @key = sanitize_key_name("#{classname}_#{name}")
@attachment = attachment
end
def has_attachment?
attachment.present?
end end
private private
......
...@@ -3406,9 +3406,6 @@ msgstr "" ...@@ -3406,9 +3406,6 @@ msgstr ""
msgid "Changing group path can have unintended side effects." msgid "Changing group path can have unintended side effects."
msgstr "" msgstr ""
msgid "Charts"
msgstr ""
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}" msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr "" msgstr ""
...@@ -19917,10 +19914,10 @@ msgstr "" ...@@ -19917,10 +19914,10 @@ msgstr ""
msgid "There was an error fetching median data for stages" msgid "There was an error fetching median data for stages"
msgstr "" msgstr ""
msgid "There was an error fetching the Designs" msgid "There was an error fetching the Node's Groups"
msgstr "" msgstr ""
msgid "There was an error fetching the Node's Groups" msgid "There was an error fetching the designs"
msgstr "" msgstr ""
msgid "There was an error fetching the environments information." msgid "There was an error fetching the environments information."
...@@ -19965,7 +19962,7 @@ msgstr "" ...@@ -19965,7 +19962,7 @@ msgstr ""
msgid "There was an error subscribing to this label." msgid "There was an error subscribing to this label."
msgstr "" msgstr ""
msgid "There was an error syncing the Design Repositories." msgid "There was an error syncing the designs."
msgstr "" msgstr ""
msgid "There was an error trying to validate your query" msgid "There was an error trying to validate your query"
......
...@@ -10,7 +10,6 @@ module QA ...@@ -10,7 +10,6 @@ module QA
element :group_settings_item element :group_settings_item
element :group_members_item element :group_members_item
element :general_settings_link element :general_settings_link
element :contribution_analytics_link
end end
view 'app/views/layouts/nav/sidebar/_analytics_links.html.haml' do view 'app/views/layouts/nav/sidebar/_analytics_links.html.haml' do
......
# frozen_string_literal: true
FactoryBot.define do
factory :test_case, class: 'Gitlab::Ci::Reports::TestCase' do
name { "test-1" }
classname { "trace" }
file { "spec/trace_spec.rb" }
execution_time { 1.23 }
status { "success" }
system_output { nil }
attachment { nil }
trait :with_attachment do
attachment { "some/path.png" }
end
skip_create
initialize_with do
new(
name: name,
classname: classname,
file: file,
execution_time: execution_time,
status: status,
system_output: system_output,
attachment: attachment
)
end
end
end
# frozen_string_literal: true
FactoryBot.define do
factory :user_highest_role do
user
end
end
...@@ -230,7 +230,7 @@ describe 'Merge request > User creates image diff notes', :js do ...@@ -230,7 +230,7 @@ describe 'Merge request > User creates image diff notes', :js do
it_behaves_like 'onion skin' it_behaves_like 'onion skin'
end end
describe 'swipe view' do describe 'swipe view', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/209999' do
before do before do
switch_to_swipe_view switch_to_swipe_view
end end
......
...@@ -7,8 +7,6 @@ describe 'Project active tab' do ...@@ -7,8 +7,6 @@ describe 'Project active tab' do
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
before do before do
stub_feature_flags(analytics_pages_under_project_analytics_sidebar: { enabled: false, thing: project })
project.add_maintainer(user) project.add_maintainer(user)
sign_in(user) sign_in(user)
end end
...@@ -45,7 +43,7 @@ describe 'Project active tab' do ...@@ -45,7 +43,7 @@ describe 'Project active tab' do
it_behaves_like 'page has active tab', 'Repository' it_behaves_like 'page has active tab', 'Repository'
%w(Files Commits Graph Compare Charts Branches Tags).each do |sub_menu| %w(Files Commits Graph Compare Branches Tags).each do |sub_menu|
context "on project Repository/#{sub_menu}" do context "on project Repository/#{sub_menu}" do
before do before do
click_tab(sub_menu) click_tab(sub_menu)
...@@ -124,11 +122,6 @@ describe 'Project active tab' do ...@@ -124,11 +122,6 @@ describe 'Project active tab' do
end end
end end
context 'when `analytics_pages_under_project_analytics_sidebar` feature flag is enabled' do
before do
stub_feature_flags(analytics_pages_under_project_analytics_sidebar: { enabled: true, thing: project })
end
context 'on project Analytics' do context 'on project Analytics' do
before do before do
visit charts_project_graph_path(project, 'master') visit charts_project_graph_path(project, 'master')
...@@ -148,5 +141,4 @@ describe 'Project active tab' do ...@@ -148,5 +141,4 @@ describe 'Project active tab' do
it_behaves_like 'page has active sub tab', _('CI / CD') it_behaves_like 'page has active sub tab', _('CI / CD')
end end
end end
end
end end
...@@ -7,8 +7,6 @@ describe 'User uses shortcuts', :js do ...@@ -7,8 +7,6 @@ describe 'User uses shortcuts', :js do
let(:user) { create(:user) } let(:user) { create(:user) }
before do before do
stub_feature_flags(analytics_pages_under_project_analytics_sidebar: { enabled: false, thing: project })
project.add_maintainer(user) project.add_maintainer(user)
sign_in(user) sign_in(user)
...@@ -119,8 +117,8 @@ describe 'User uses shortcuts', :js do ...@@ -119,8 +117,8 @@ describe 'User uses shortcuts', :js do
find('body').native.send_key('g') find('body').native.send_key('g')
find('body').native.send_key('d') find('body').native.send_key('d')
expect(page).to have_active_navigation('Repository') expect(page).to have_active_navigation(_('Analytics'))
expect(page).to have_active_sub_navigation('Charts') expect(page).to have_active_sub_navigation(_('Repository'))
end end
end end
...@@ -211,18 +209,4 @@ describe 'User uses shortcuts', :js do ...@@ -211,18 +209,4 @@ describe 'User uses shortcuts', :js do
expect(page).to have_active_navigation('Wiki') expect(page).to have_active_navigation('Wiki')
end end
end end
context 'when `analytics_pages_under_project_analytics_sidebar` feature flag is enabled' do
before do
stub_feature_flags(analytics_pages_under_project_analytics_sidebar: { enabled: true, thing: project })
end
it 'redirects to the repository charts page' do
find('body').native.send_key('g')
find('body').native.send_key('d')
expect(page).to have_active_navigation(_('Analytics'))
expect(page).to have_active_sub_navigation(_('Repository'))
end
end
end end
...@@ -205,6 +205,75 @@ describe Gitlab::Ci::Parsers::Test::Junit do ...@@ -205,6 +205,75 @@ describe Gitlab::Ci::Parsers::Test::Junit do
end end
end end
context 'when data contains an attachment tag' do
let(:junit) do
<<~EOF
<testsuites>
<testsuite>
<testcase classname='Calculator' name='sumTest1' time='0.01'>
<failure>Some failure</failure>
<system-out>[[ATTACHMENT|some/path.png]]</system-out>
</testcase>
</testsuite>
</testsuites>
EOF
end
it 'add attachment to a test case' do
expect { subject }.not_to raise_error
expect(test_cases[0].has_attachment?).to be_truthy
expect(test_cases[0].attachment).to eq("some/path.png")
end
end
context 'when data contains multiple attachments tag' do
let(:junit) do
<<~EOF
<testsuites>
<testsuite>
<testcase classname='Calculator' name='sumTest1' time='0.01'>
<failure>Some failure</failure>
<system-out>
[[ATTACHMENT|some/path.png]]
[[ATTACHMENT|some/path.html]]
</system-out>
</testcase>
</testsuite>
</testsuites>
EOF
end
it 'adds the first match attachment to a test case' do
expect { subject }.not_to raise_error
expect(test_cases[0].has_attachment?).to be_truthy
expect(test_cases[0].attachment).to eq("some/path.png")
end
end
context 'when data does not match attachment tag regex' do
let(:junit) do
<<~EOF
<testsuites>
<testsuite>
<testcase classname='Calculator' name='sumTest1' time='0.01'>
<failure>Some failure</failure>
<system-out>[[attachment]some/path.png]]</system-out>
</testcase>
</testsuite>
</testsuites>
EOF
end
it 'does not add attachment to a test case' do
expect { subject }.not_to raise_error
expect(test_cases[0].has_attachment?).to be_falsy
expect(test_cases[0].attachment).to be_nil
end
end
private private
def flattened_test_cases(test_suite) def flattened_test_cases(test_suite)
......
...@@ -88,5 +88,17 @@ describe Gitlab::Ci::Reports::TestCase do ...@@ -88,5 +88,17 @@ describe Gitlab::Ci::Reports::TestCase do
expect { test_case }.to raise_error(ArgumentError) expect { test_case }.to raise_error(ArgumentError)
end end
end end
context 'when attachment is present' do
let(:attachment_test_case) { build(:test_case, :with_attachment) }
it "initializes the attachment if present" do
expect(attachment_test_case.attachment).to eq("some/path.png")
end
it '#has_attachment?' do
expect(attachment_test_case.has_attachment?).to be_truthy
end
end
end end
end end
...@@ -124,7 +124,7 @@ describe Gitlab::SidekiqConfig::CliMethods do ...@@ -124,7 +124,7 @@ describe Gitlab::SidekiqConfig::CliMethods do
name: 'a', name: 'a',
feature_category: :category_a, feature_category: :category_a,
has_external_dependencies: false, has_external_dependencies: false,
urgency: :default, urgency: :low,
resource_boundary: :cpu resource_boundary: :cpu
}, },
{ {
...@@ -145,7 +145,7 @@ describe Gitlab::SidekiqConfig::CliMethods do ...@@ -145,7 +145,7 @@ describe Gitlab::SidekiqConfig::CliMethods do
name: 'c', name: 'c',
feature_category: :category_c, feature_category: :category_c,
has_external_dependencies: false, has_external_dependencies: false,
urgency: :none, urgency: :throttled,
resource_boundary: :memory resource_boundary: :memory
} }
] ]
...@@ -168,9 +168,9 @@ describe Gitlab::SidekiqConfig::CliMethods do ...@@ -168,9 +168,9 @@ describe Gitlab::SidekiqConfig::CliMethods do
# urgency # urgency
'urgency=high' | %w(a:2 b) 'urgency=high' | %w(a:2 b)
'urgency=default' | %w(a) 'urgency=low' | %w(a)
'urgency=high,default,none' | %w(a a:2 b c) 'urgency=high,low,throttled' | %w(a a:2 b c)
'urgency=default|urgency=none' | %w(a c) 'urgency=low|urgency=throttled' | %w(a c)
'urgency!=high' | %w(a c) 'urgency!=high' | %w(a c)
# name # name
......
...@@ -88,7 +88,7 @@ describe Gitlab::SidekiqConfig::Worker do ...@@ -88,7 +88,7 @@ describe Gitlab::SidekiqConfig::Worker do
attributes_a = { attributes_a = {
feature_category: :source_code_management, feature_category: :source_code_management,
has_external_dependencies: false, has_external_dependencies: false,
urgency: :default, urgency: :low,
resource_boundary: :memory, resource_boundary: :memory,
weight: 2, weight: 2,
idempotent: true idempotent: true
......
...@@ -9,7 +9,7 @@ describe Gitlab::SidekiqMiddleware::ClientMetrics do ...@@ -9,7 +9,7 @@ describe Gitlab::SidekiqMiddleware::ClientMetrics do
let(:queue) { :test } let(:queue) { :test }
let(:worker_class) { worker.class } let(:worker_class) { worker.class }
let(:job) { {} } let(:job) { {} }
let(:default_labels) { { queue: queue.to_s, boundary: "", external_dependencies: "no", feature_category: "", urgency: "default" } } let(:default_labels) { { queue: queue.to_s, boundary: "", external_dependencies: "no", feature_category: "", urgency: "low" } }
shared_examples "a metrics client middleware" do shared_examples "a metrics client middleware" do
context "with mocked prometheus" do context "with mocked prometheus" do
...@@ -80,8 +80,8 @@ describe Gitlab::SidekiqMiddleware::ClientMetrics do ...@@ -80,8 +80,8 @@ describe Gitlab::SidekiqMiddleware::ClientMetrics do
context "no urgency" do context "no urgency" do
it_behaves_like "a metrics client middleware" do it_behaves_like "a metrics client middleware" do
let(:urgency) { :none } let(:urgency) { :throttled }
let(:labels) { default_labels.merge(urgency: "none") } let(:labels) { default_labels.merge(urgency: "throttled") }
end end
end end
......
...@@ -11,7 +11,7 @@ describe Gitlab::SidekiqMiddleware::ServerMetrics do ...@@ -11,7 +11,7 @@ describe Gitlab::SidekiqMiddleware::ServerMetrics do
let(:job) { {} } let(:job) { {} }
let(:job_status) { :done } let(:job_status) { :done }
let(:labels_with_job_status) { labels.merge(job_status: job_status.to_s) } let(:labels_with_job_status) { labels.merge(job_status: job_status.to_s) }
let(:default_labels) { { queue: queue.to_s, boundary: "", external_dependencies: "no", feature_category: "", urgency: "default" } } let(:default_labels) { { queue: queue.to_s, boundary: "", external_dependencies: "no", feature_category: "", urgency: "low" } }
shared_examples "a metrics middleware" do shared_examples "a metrics middleware" do
context "with mocked prometheus" do context "with mocked prometheus" do
...@@ -202,11 +202,11 @@ describe Gitlab::SidekiqMiddleware::ServerMetrics do ...@@ -202,11 +202,11 @@ describe Gitlab::SidekiqMiddleware::ServerMetrics do
end end
context "combined" do context "combined" do
let(:urgency) { :none } let(:urgency) { :throttled }
let(:external_dependencies) { true } let(:external_dependencies) { true }
let(:resource_boundary) { :cpu } let(:resource_boundary) { :cpu }
let(:feature_category) { :authentication } let(:feature_category) { :authentication }
let(:labels) { default_labels.merge(urgency: "none", external_dependencies: "yes", boundary: "cpu", feature_category: "authentication") } let(:labels) { default_labels.merge(urgency: "throttled", external_dependencies: "yes", boundary: "cpu", feature_category: "authentication") }
it_behaves_like "a metrics middleware" it_behaves_like "a metrics middleware"
end end
......
...@@ -201,6 +201,26 @@ describe Namespace do ...@@ -201,6 +201,26 @@ describe Namespace do
expect(described_class.find_by_pages_host(host)).to eq(namespace) expect(described_class.find_by_pages_host(host)).to eq(namespace)
end end
context 'when there is non-top-level group with searched name' do
before do
create(:group, :nested, path: 'pages')
end
it 'ignores this group' do
host = "pages.#{Settings.pages.host.upcase}"
expect(described_class.find_by_pages_host(host)).to be_nil
end
it 'finds right top level group' do
group = create(:group, path: 'pages')
host = "pages.#{Settings.pages.host.upcase}"
expect(described_class.find_by_pages_host(host)).to eq(group)
end
end
it "returns no result if the provided host is not subdomain of the Pages host" do it "returns no result if the provided host is not subdomain of the Pages host" do
create(:namespace, name: 'namespace.io') create(:namespace, name: 'namespace.io')
host = "namespace.io" host = "namespace.io"
......
# frozen_string_literal: true
require 'spec_helper'
describe UserHighestRole do
describe 'associations' do
it { is_expected.to belong_to(:user).required }
end
describe 'validations' do
it { is_expected.to validate_inclusion_of(:highest_access_level).in_array([nil, *Gitlab::Access.all_values]) }
end
end
...@@ -30,6 +30,7 @@ describe User, :do_not_mock_admin_mode do ...@@ -30,6 +30,7 @@ describe User, :do_not_mock_admin_mode do
it { is_expected.to have_one(:status) } it { is_expected.to have_one(:status) }
it { is_expected.to have_one(:max_access_level_membership) } it { is_expected.to have_one(:max_access_level_membership) }
it { is_expected.to have_one(:user_detail) } it { is_expected.to have_one(:user_detail) }
it { is_expected.to have_one(:user_highest_role) }
it { is_expected.to have_many(:snippets).dependent(:destroy) } it { is_expected.to have_many(:snippets).dependent(:destroy) }
it { is_expected.to have_many(:members) } it { is_expected.to have_many(:members) }
it { is_expected.to have_many(:project_members) } it { is_expected.to have_many(:project_members) }
......
...@@ -48,7 +48,7 @@ describe API::Groups do ...@@ -48,7 +48,7 @@ describe API::Groups do
context 'when file format is not supported' do context 'when file format is not supported' do
let(:file_path) { 'spec/fixtures/doc_sample.txt' } let(:file_path) { 'spec/fixtures/doc_sample.txt' }
let(:message) { 'file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff, ico' } let(:message) { 'file format is not supported. Please try one of the following supported formats: image/png, image/jpeg, image/gif, image/bmp, image/tiff, image/vnd.microsoft.icon' }
it_behaves_like 'invalid file upload request' it_behaves_like 'invalid file upload request'
end end
......
...@@ -20,6 +20,7 @@ RSpec.shared_context 'uploader with type check' do ...@@ -20,6 +20,7 @@ RSpec.shared_context 'uploader with type check' do
end end
end end
# This works with the UploadTypeCheck::Concern
RSpec.shared_context 'stubbed MimeMagic mime type detection' do RSpec.shared_context 'stubbed MimeMagic mime type detection' do
let(:mime_type) { '' } let(:mime_type) { '' }
let(:magic_mime) { mime_type } let(:magic_mime) { mime_type }
...@@ -31,3 +32,19 @@ RSpec.shared_context 'stubbed MimeMagic mime type detection' do ...@@ -31,3 +32,19 @@ RSpec.shared_context 'stubbed MimeMagic mime type detection' do
allow(MimeMagic).to receive(:by_path).with(anything).and_return(ext_mime_obj) allow(MimeMagic).to receive(:by_path).with(anything).and_return(ext_mime_obj)
end end
end end
# @param uploader [CarrierWave::Uploader::Base] uploader with extension_whitelist method.
RSpec.shared_context 'ignore extension whitelist check' do
before do
allow(uploader).to receive(:extension_whitelist).and_return(nil)
end
end
# This works with a content_type_whitelist and content_type_blacklist type check.
# @param mime_type [String] mime type to forcibly detect.
RSpec.shared_context 'force content type detection to mime_type' do
before do
magic_mime_obj = MimeMagic.new(mime_type)
allow(MimeMagic).to receive(:by_magic).with(anything).and_return(magic_mime_obj)
end
end
# frozen_string_literal: true # frozen_string_literal: true
# @param path [String] the path to file to upload. E.g. File.join('spec', 'fixtures', 'sanitized.svg')
# @param uploader [CarrierWave::Uploader::Base] uploader to handle the upload.
shared_examples 'denied carrierwave upload' do
it 'will deny upload' do
fixture_file = fixture_file_upload(path)
expect { uploader.cache!(fixture_file) }.to raise_exception(CarrierWave::IntegrityError)
end
end
# @param path [String] the path to file to upload. E.g. File.join('spec', 'fixtures', 'sanitized.svg')
# @param uploader [CarrierWave::Uploader::Base] uploader to handle the upload.
shared_examples 'accepted carrierwave upload' do
let(:fixture_file) { fixture_file_upload(path) }
before do
uploader.remove!
end
it 'will accept upload' do
expect { uploader.cache!(fixture_file) }.not_to raise_exception
end
it 'will cache uploaded file' do
expect { uploader.cache!(fixture_file) }.to change { uploader.file }.from(nil).to(kind_of(CarrierWave::SanitizedFile))
end
end
def check_content_matches_extension!(file = double(read: nil, path: '')) def check_content_matches_extension!(file = double(read: nil, path: ''))
magic_file = UploadTypeCheck::MagicFile.new(file) magic_file = UploadTypeCheck::MagicFile.new(file)
uploader.check_content_matches_extension!(magic_file) uploader.check_content_matches_extension!(magic_file)
......
...@@ -47,15 +47,29 @@ describe AvatarUploader do ...@@ -47,15 +47,29 @@ describe AvatarUploader do
end end
end end
context 'upload type check' do context 'accept whitelist file content type' do
AvatarUploader::SAFE_IMAGE_EXT.each do |ext| # We need to feed through a valid path, but we force the parsed mime type
context "#{ext} extension" do # in a stub below so we can set any path.
it_behaves_like 'type checked uploads', filenames: "image.#{ext}" let_it_be(:path) { File.join('spec', 'fixtures', 'video_sample.mp4') }
where(:mime_type) { described_class::MIME_WHITELIST }
with_them do
include_context 'force content type detection to mime_type'
it_behaves_like 'accepted carrierwave upload'
end end
end end
context 'skip image/svg+xml integrity check' do context 'upload non-whitelisted file content type' do
it_behaves_like 'skipped type checked uploads', filenames: 'image.svg' let_it_be(:path) { File.join('spec', 'fixtures', 'sanitized.svg') }
it_behaves_like 'denied carrierwave upload'
end end
context 'upload misnamed non-whitelisted file content type' do
let_it_be(:path) { File.join('spec', 'fixtures', 'not_a_png.png') }
it_behaves_like 'denied carrierwave upload'
end end
end end
# frozen_string_literal: true
require 'spec_helper'
describe ContentTypeWhitelist do
class DummyUploader < CarrierWave::Uploader::Base
include ContentTypeWhitelist::Concern
def content_type_whitelist
%w[image/png image/jpeg]
end
end
let_it_be(:model) { build_stubbed(:user) }
let_it_be(:uploader) { DummyUploader.new(model, :dummy) }
context 'upload whitelisted file content type' do
let(:path) { File.join('spec', 'fixtures', 'rails_sample.jpg') }
it_behaves_like 'accepted carrierwave upload'
end
context 'upload non-whitelisted file content type' do
let(:path) { File.join('spec', 'fixtures', 'sanitized.svg') }
it_behaves_like 'denied carrierwave upload'
end
context 'upload misnamed non-whitelisted file content type' do
let(:path) { File.join('spec', 'fixtures', 'not_a_png.png') }
it_behaves_like 'denied carrierwave upload'
end
end
...@@ -6,19 +6,35 @@ describe FaviconUploader do ...@@ -6,19 +6,35 @@ describe FaviconUploader do
let_it_be(:model) { build_stubbed(:user) } let_it_be(:model) { build_stubbed(:user) }
let_it_be(:uploader) { described_class.new(model, :favicon) } let_it_be(:uploader) { described_class.new(model, :favicon) }
context 'upload type check' do context 'accept whitelist file content type' do
FaviconUploader::EXTENSION_WHITELIST.each do |ext| include_context 'ignore extension whitelist check'
context "#{ext} extension" do
it_behaves_like 'type checked uploads', filenames: "image.#{ext}" # We need to feed through a valid path, but we force the parsed mime type
end # in a stub below so we can set any path.
let_it_be(:path) { File.join('spec', 'fixtures', 'video_sample.mp4') }
where(:mime_type) { described_class::MIME_WHITELIST }
with_them do
include_context 'force content type detection to mime_type'
it_behaves_like 'accepted carrierwave upload'
end end
end end
context 'upload non-whitelisted file extensions' do context 'upload non-whitelisted file content type' do
it 'will deny upload' do include_context 'ignore extension whitelist check'
path = File.join('spec', 'fixtures', 'banana_sample.gif')
fixture_file = fixture_file_upload(path) let_it_be(:path) { File.join('spec', 'fixtures', 'sanitized.svg') }
expect { uploader.cache!(fixture_file) }.to raise_exception(CarrierWave::IntegrityError)
it_behaves_like 'denied carrierwave upload'
end end
context 'upload misnamed non-whitelisted file content type' do
include_context 'ignore extension whitelist check'
let_it_be(:path) { File.join('spec', 'fixtures', 'not_a_png.png') }
it_behaves_like 'denied carrierwave upload'
end end
end end
...@@ -166,7 +166,6 @@ describe 'layouts/nav/sidebar/_project' do ...@@ -166,7 +166,6 @@ describe 'layouts/nav/sidebar/_project' do
before do before do
allow(view).to receive(:can?).with(nil, :read_cycle_analytics, project).and_return(read_cycle_analytics) allow(view).to receive(:can?).with(nil, :read_cycle_analytics, project).and_return(read_cycle_analytics)
stub_feature_flags(analytics_pages_under_project_analytics_sidebar: { enabled: false, thing: project })
end end
describe 'when value stream analytics is enabled' do describe 'when value stream analytics is enabled' do
......
...@@ -26,17 +26,5 @@ describe UpdateMergeRequestsWorker do ...@@ -26,17 +26,5 @@ describe UpdateMergeRequestsWorker do
perform perform
end end
context 'when slow' do
before do
stub_const("UpdateMergeRequestsWorker::LOG_TIME_THRESHOLD", -1)
end
it 'logs debug info' do
expect(Rails.logger).to receive(:info).with(a_string_matching(/\AUpdateMergeRequestsWorker#perform.*project_id=#{project.id},user_id=#{user.id},oldrev=#{oldrev},newrev=#{newrev},ref=#{ref}/))
perform
end
end
end end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment