Update upstream source from tag 'upstream/11.10.8+dfsg'
Update to upstream version '11.10.8+dfsg'
with Debian dir 890e9ebfea
This commit is contained in:
commit
ad11324c19
105 changed files with 1496 additions and 450 deletions
|
@ -1,4 +1,4 @@
|
|||
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.5.3-golang-1.11-git-2.18-chrome-71.0-node-10.x-yarn-1.12-postgresql-9.6-graphicsmagick-1.3.29"
|
||||
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.5.3-golang-1.11-git-2.18-chrome-73.0-node-10.x-yarn-1.12-postgresql-9.6-graphicsmagick-1.3.29"
|
||||
|
||||
include:
|
||||
- local: /lib/gitlab/ci/templates/Code-Quality.gitlab-ci.yml
|
||||
|
@ -655,7 +655,7 @@ gitlab:setup-mysql:
|
|||
# Frontend-related jobs
|
||||
gitlab:assets:compile:
|
||||
<<: *dedicated-no-docs-pull-cache-job
|
||||
image: dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.5.3-git-2.18-chrome-71.0-node-8.x-yarn-1.12-graphicsmagick-1.3.29-docker-18.06.1
|
||||
image: dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.5.3-git-2.18-chrome-73.0-node-8.x-yarn-1.12-graphicsmagick-1.3.29-docker-18.06.1
|
||||
dependencies:
|
||||
- setup-test-env
|
||||
services:
|
||||
|
|
47
CHANGELOG.md
47
CHANGELOG.md
|
@ -2,6 +2,53 @@
|
|||
documentation](doc/development/changelog.md) for instructions on adding your own
|
||||
entry.
|
||||
|
||||
## 11.10.8 (2019-06-27)
|
||||
|
||||
- No changes.
|
||||
### Security (10 changes)
|
||||
|
||||
- Fix Denial of Service for comments when rendering issues/MR comments.
|
||||
- Gate MR head_pipeline behind read_pipeline ability.
|
||||
- Fix DoS vulnerability in color validation regex.
|
||||
- Expose merge requests count based on user access.
|
||||
- Persist tmp snippet uploads at users.
|
||||
- Add missing authorizations in GraphQL.
|
||||
- Disable Rails SQL query cache when applying service templates.
|
||||
- Prevent Billion Laughs attack.
|
||||
- Correctly check permissions when creating snippet notes.
|
||||
- Prevent the detection of merge request templates by unauthorized users.
|
||||
|
||||
### Performance (1 change)
|
||||
|
||||
- Add improvements to global search of issues and merge requests. !27817
|
||||
|
||||
|
||||
## 11.10.7 (2019-06-26)
|
||||
|
||||
### Fixed (3 changes)
|
||||
|
||||
- Remove a default git depth in Pipelines for merge requests. !28926
|
||||
- Fix label click scrolling to top. !29202
|
||||
- Fix scrolling to top on assignee change. !29500
|
||||
|
||||
|
||||
## 11.10.6 (2019-06-04)
|
||||
|
||||
### Fixed (7 changes, 1 of them is from the community)
|
||||
|
||||
- Allow a member to have an access level equal to parent group. !27913
|
||||
- Fix uploading of LFS tracked file through UI. !28052
|
||||
- Use 3-way merge for squashing commits. !28078
|
||||
- Use a path for the related merge requests endpoint. !28171
|
||||
- Fix project visibility level validation. !28305 (Peter Marko)
|
||||
- Fix Rugged get_tree_entries recursive flag not working. !28494
|
||||
- Use source ref in pipeline webhook. !28772
|
||||
|
||||
### Other (1 change)
|
||||
|
||||
- Fix input group height.
|
||||
|
||||
|
||||
## 11.10.5 (2019-05-30)
|
||||
|
||||
### Security (12 changes, 1 of them is from the community)
|
||||
|
|
|
@ -1 +1 @@
|
|||
1.34.1
|
||||
1.34.3
|
||||
|
|
2
Gemfile
2
Gemfile
|
@ -419,7 +419,7 @@ group :ed25519 do
|
|||
end
|
||||
|
||||
# Gitaly GRPC client
|
||||
gem 'gitaly-proto', '~> 1.19.0', require: 'gitaly'
|
||||
gem 'gitaly-proto', '~> 1.22.1', require: 'gitaly'
|
||||
|
||||
gem 'grpc', '~> 1.15.0'
|
||||
|
||||
|
|
|
@ -281,7 +281,7 @@ GEM
|
|||
gettext_i18n_rails (>= 0.7.1)
|
||||
po_to_json (>= 1.0.0)
|
||||
rails (>= 3.2.0)
|
||||
gitaly-proto (1.19.0)
|
||||
gitaly-proto (1.22.1)
|
||||
grpc (~> 1.0)
|
||||
github-markup (1.7.0)
|
||||
gitlab-default_value_for (3.1.1)
|
||||
|
@ -1020,7 +1020,7 @@ DEPENDENCIES
|
|||
gettext (~> 3.2.2)
|
||||
gettext_i18n_rails (~> 1.8.0)
|
||||
gettext_i18n_rails_js (~> 1.3)
|
||||
gitaly-proto (~> 1.19.0)
|
||||
gitaly-proto (~> 1.22.1)
|
||||
github-markup (~> 1.7.0)
|
||||
gitlab-default_value_for (~> 3.1.1)
|
||||
gitlab-markup (~> 1.7.0)
|
||||
|
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
11.10.5
|
||||
11.10.8
|
||||
|
|
|
@ -4,6 +4,11 @@ import { generateTreeList } from '../store/utils';
|
|||
// eslint-disable-next-line no-restricted-globals
|
||||
self.addEventListener('message', e => {
|
||||
const { data } = e;
|
||||
|
||||
if (data === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { treeEntries, tree } = generateTreeList(data);
|
||||
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
|
|
|
@ -561,6 +561,11 @@ GitLabDropdown = (function() {
|
|||
!$target.data('isLink')
|
||||
) {
|
||||
e.stopPropagation();
|
||||
|
||||
// This prevents automatic scrolling to the top
|
||||
if ($target.closest('a').length) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
|
@ -275,3 +275,7 @@ label {
|
|||
max-width: $input-lg-width;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input-group-text {
|
||||
max-height: $input-height;
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ $secondary: $gray-light;
|
|||
$input-disabled-bg: $gray-light;
|
||||
$input-border-color: $gray-200;
|
||||
$input-color: $gl-text-color;
|
||||
$input-font-size: $gl-font-size;
|
||||
$font-family-sans-serif: $regular-font;
|
||||
$font-family-monospace: $monospace-font;
|
||||
$btn-line-height: 20px;
|
||||
|
|
|
@ -75,6 +75,8 @@ input[type='checkbox']:hover {
|
|||
}
|
||||
|
||||
.search-input-wrap {
|
||||
width: 100%;
|
||||
|
||||
.search-icon,
|
||||
.clear-icon {
|
||||
position: absolute;
|
||||
|
|
|
@ -41,7 +41,7 @@ module IssuableCollections
|
|||
return if pagination_disabled?
|
||||
|
||||
@issuables = @issuables.page(params[:page])
|
||||
@issuable_meta_data = issuable_meta_data(@issuables, collection_type)
|
||||
@issuable_meta_data = issuable_meta_data(@issuables, collection_type, current_user)
|
||||
@total_pages = issuable_page_count
|
||||
end
|
||||
# rubocop:enable Gitlab/ModuleWithInstanceVariables
|
||||
|
|
|
@ -11,7 +11,7 @@ module IssuableCollectionsAction
|
|||
.non_archived
|
||||
.page(params[:page])
|
||||
|
||||
@issuable_meta_data = issuable_meta_data(@issues, collection_type)
|
||||
@issuable_meta_data = issuable_meta_data(@issues, collection_type, current_user)
|
||||
|
||||
respond_to do |format|
|
||||
format.html
|
||||
|
@ -22,7 +22,7 @@ module IssuableCollectionsAction
|
|||
def merge_requests
|
||||
@merge_requests = issuables_collection.page(params[:page])
|
||||
|
||||
@issuable_meta_data = issuable_meta_data(@merge_requests, collection_type)
|
||||
@issuable_meta_data = issuable_meta_data(@merge_requests, collection_type, current_user)
|
||||
end
|
||||
# rubocop:enable Gitlab/ModuleWithInstanceVariables
|
||||
|
||||
|
|
|
@ -203,17 +203,17 @@ module NotesActions
|
|||
|
||||
# These params are also sent by the client but we need to set these based on
|
||||
# target_type and target_id because we're checking permissions based on that
|
||||
create_params[:noteable_type] = params[:target_type].classify
|
||||
create_params[:noteable_type] = noteable.class.name
|
||||
|
||||
case params[:target_type]
|
||||
when 'commit'
|
||||
create_params[:commit_id] = params[:target_id]
|
||||
when 'merge_request'
|
||||
create_params[:noteable_id] = params[:target_id]
|
||||
case noteable
|
||||
when Commit
|
||||
create_params[:commit_id] = noteable.id
|
||||
when MergeRequest
|
||||
create_params[:noteable_id] = noteable.id
|
||||
# Notes on MergeRequest can have an extra `commit_id` context
|
||||
create_params[:commit_id] = params.dig(:note, :commit_id)
|
||||
else
|
||||
create_params[:noteable_id] = params[:target_id]
|
||||
create_params[:noteable_id] = noteable.id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -13,6 +13,11 @@ class Projects::ApplicationController < ApplicationController
|
|||
|
||||
helper_method :repository, :can_collaborate_with_project?, :user_access
|
||||
|
||||
rescue_from Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError do |exception|
|
||||
log_exception(exception)
|
||||
render_404
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def project
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Projects::TemplatesController < Projects::ApplicationController
|
||||
before_action :authenticate_user!, :get_template_class
|
||||
before_action :authenticate_user!
|
||||
before_action :authorize_can_read_issuable!
|
||||
before_action :get_template_class
|
||||
|
||||
def show
|
||||
template = @template_type.find(params[:key], project)
|
||||
|
@ -13,9 +15,20 @@ class Projects::TemplatesController < Projects::ApplicationController
|
|||
|
||||
private
|
||||
|
||||
# User must have:
|
||||
# - `read_merge_request` to see merge request templates, or
|
||||
# - `read_issue` to see issue templates
|
||||
#
|
||||
# Note params[:template_type] has a route constraint to limit it to
|
||||
# `merge_request` or `issue`
|
||||
def authorize_can_read_issuable!
|
||||
action = [:read_, params[:template_type]].join
|
||||
|
||||
authorize_action!(action)
|
||||
end
|
||||
|
||||
def get_template_class
|
||||
template_types = { issue: Gitlab::Template::IssueTemplate, merge_request: Gitlab::Template::MergeRequestTemplate }.with_indifferent_access
|
||||
@template_type = template_types[params[:template_type]]
|
||||
render json: [], status: :not_found unless @template_type
|
||||
end
|
||||
end
|
||||
|
|
|
@ -297,7 +297,7 @@ class ProjectsController < Projects::ApplicationController
|
|||
elsif @project.feature_available?(:issues, current_user)
|
||||
@issues = issuables_collection.page(params[:page])
|
||||
@collection_type = 'Issue'
|
||||
@issuable_meta_data = issuable_meta_data(@issues, @collection_type)
|
||||
@issuable_meta_data = issuable_meta_data(@issues, @collection_type, current_user)
|
||||
end
|
||||
|
||||
render :show
|
||||
|
|
|
@ -5,8 +5,8 @@ class Snippets::NotesController < ApplicationController
|
|||
include ToggleAwardEmoji
|
||||
|
||||
skip_before_action :authenticate_user!, only: [:index]
|
||||
before_action :snippet
|
||||
before_action :authorize_read_snippet!, only: [:show, :index, :create]
|
||||
before_action :authorize_read_snippet!, only: [:show, :index]
|
||||
before_action :authorize_create_note!, only: [:create]
|
||||
|
||||
private
|
||||
|
||||
|
@ -33,4 +33,8 @@ class Snippets::NotesController < ApplicationController
|
|||
def authorize_read_snippet!
|
||||
return render_404 unless can?(current_user, :read_personal_snippet, snippet)
|
||||
end
|
||||
|
||||
def authorize_create_note!
|
||||
access_denied! unless can?(current_user, :create_note, noteable)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -137,7 +137,7 @@ class SnippetsController < ApplicationController
|
|||
|
||||
def move_temporary_files
|
||||
params[:files].each do |file|
|
||||
FileMover.new(file, @snippet).execute
|
||||
FileMover.new(file, from_model: current_user, to_model: @snippet).execute
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -41,7 +41,11 @@ class UploadsController < ApplicationController
|
|||
when Note
|
||||
can?(current_user, :read_project, model.project)
|
||||
when User
|
||||
true
|
||||
# We validate the current user has enough (writing)
|
||||
# access to itself when a secret is given.
|
||||
# For instance, user avatars are readable by anyone,
|
||||
# while temporary, user snippet uploads are not.
|
||||
!secret? || can?(current_user, :update_user, model)
|
||||
when Appearance
|
||||
true
|
||||
else
|
||||
|
@ -56,8 +60,13 @@ class UploadsController < ApplicationController
|
|||
def authorize_create_access!
|
||||
return unless model
|
||||
|
||||
# for now we support only personal snippets comments
|
||||
authorized = can?(current_user, :comment_personal_snippet, model)
|
||||
authorized =
|
||||
case model
|
||||
when User
|
||||
can?(current_user, :update_user, model)
|
||||
else
|
||||
can?(current_user, :comment_personal_snippet, model)
|
||||
end
|
||||
|
||||
render_unauthorized unless authorized
|
||||
end
|
||||
|
@ -74,6 +83,10 @@ class UploadsController < ApplicationController
|
|||
User === model || Appearance === model
|
||||
end
|
||||
|
||||
def secret?
|
||||
params[:secret].present?
|
||||
end
|
||||
|
||||
def upload_model_class
|
||||
MODEL_CLASSES[params[:model]] || raise(UnknownUploadModelError)
|
||||
end
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
# updated_after: datetime
|
||||
# updated_before: datetime
|
||||
# attempt_group_search_optimizations: boolean
|
||||
# attempt_project_search_optimizations: boolean
|
||||
#
|
||||
class IssuableFinder
|
||||
prepend FinderWithCrossProjectAccess
|
||||
|
@ -184,7 +185,6 @@ class IssuableFinder
|
|||
@project = project
|
||||
end
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def projects
|
||||
return @projects if defined?(@projects)
|
||||
|
||||
|
@ -192,17 +192,25 @@ class IssuableFinder
|
|||
|
||||
projects =
|
||||
if current_user && params[:authorized_only].presence && !current_user_related?
|
||||
current_user.authorized_projects
|
||||
current_user.authorized_projects(min_access_level)
|
||||
elsif group
|
||||
finder_options = { include_subgroups: params[:include_subgroups], only_owned: true }
|
||||
GroupProjectsFinder.new(group: group, current_user: current_user, options: finder_options).execute # rubocop: disable CodeReuse/Finder
|
||||
find_group_projects
|
||||
else
|
||||
ProjectsFinder.new(current_user: current_user).execute # rubocop: disable CodeReuse/Finder
|
||||
Project.public_or_visible_to_user(current_user, min_access_level)
|
||||
end
|
||||
|
||||
@projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil)
|
||||
@projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil) # rubocop: disable CodeReuse/ActiveRecord
|
||||
end
|
||||
|
||||
def find_group_projects
|
||||
return Project.none unless group
|
||||
|
||||
if params[:include_subgroups]
|
||||
Project.where(namespace_id: group.self_and_descendants) # rubocop: disable CodeReuse/ActiveRecord
|
||||
else
|
||||
group.projects
|
||||
end.public_or_visible_to_user(current_user, min_access_level)
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
def search
|
||||
params[:search].presence
|
||||
|
@ -572,4 +580,8 @@ class IssuableFinder
|
|||
scope = params[:scope]
|
||||
scope == 'created_by_me' || scope == 'authored' || scope == 'assigned_to_me'
|
||||
end
|
||||
|
||||
def min_access_level
|
||||
ProjectFeature.required_minimum_access_level(klass)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -48,9 +48,9 @@ class IssuesFinder < IssuableFinder
|
|||
OR (issues.confidential = TRUE
|
||||
AND (issues.author_id = :user_id
|
||||
OR EXISTS (SELECT TRUE FROM issue_assignees WHERE user_id = :user_id AND issue_id = issues.id)
|
||||
OR issues.project_id IN(:project_ids)))',
|
||||
OR EXISTS (:authorizations)))',
|
||||
user_id: current_user.id,
|
||||
project_ids: current_user.authorized_projects(CONFIDENTIAL_ACCESS_LEVEL).select(:id))
|
||||
authorizations: current_user.authorizations_for_projects(min_access_level: CONFIDENTIAL_ACCESS_LEVEL, related_project_column: "issues.project_id"))
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
|
|
|
@ -62,7 +62,7 @@ class ProjectsFinder < UnionFinder
|
|||
collection = by_personal(collection)
|
||||
collection = by_starred(collection)
|
||||
collection = by_trending(collection)
|
||||
collection = by_visibilty_level(collection)
|
||||
collection = by_visibility_level(collection)
|
||||
collection = by_tags(collection)
|
||||
collection = by_search(collection)
|
||||
collection = by_archived(collection)
|
||||
|
@ -71,12 +71,11 @@ class ProjectsFinder < UnionFinder
|
|||
collection
|
||||
end
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def collection_with_user
|
||||
if owned_projects?
|
||||
current_user.owned_projects
|
||||
elsif min_access_level?
|
||||
current_user.authorized_projects.where('project_authorizations.access_level >= ?', params[:min_access_level])
|
||||
current_user.authorized_projects(params[:min_access_level])
|
||||
else
|
||||
if private_only?
|
||||
current_user.authorized_projects
|
||||
|
@ -85,7 +84,6 @@ class ProjectsFinder < UnionFinder
|
|||
end
|
||||
end
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
# Builds a collection for an anonymous user.
|
||||
def collection_without_user
|
||||
|
@ -131,7 +129,7 @@ class ProjectsFinder < UnionFinder
|
|||
end
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def by_visibilty_level(items)
|
||||
def by_visibility_level(items)
|
||||
params[:visibility_level].present? ? items.where(visibility_level: params[:visibility_level]) : items
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
|
|
@ -4,6 +4,8 @@ module Types
|
|||
class LabelType < BaseObject
|
||||
graphql_name 'Label'
|
||||
|
||||
authorize :read_label
|
||||
|
||||
field :description, GraphQL::STRING_TYPE, null: true
|
||||
field :title, GraphQL::STRING_TYPE, null: false
|
||||
field :color, GraphQL::STRING_TYPE, null: false
|
||||
|
|
|
@ -4,6 +4,8 @@ module Types
|
|||
class MetadataType < ::Types::BaseObject
|
||||
graphql_name 'Metadata'
|
||||
|
||||
authorize :read_instance_metadata
|
||||
|
||||
field :version, GraphQL::STRING_TYPE, null: false
|
||||
field :revision, GraphQL::STRING_TYPE, null: false
|
||||
end
|
||||
|
|
|
@ -12,10 +12,7 @@ module Types
|
|||
field :metadata, Types::MetadataType,
|
||||
null: true,
|
||||
resolver: Resolvers::MetadataResolver,
|
||||
description: 'Metadata about GitLab' do |*args|
|
||||
|
||||
authorize :read_instance_metadata
|
||||
end
|
||||
description: 'Metadata about GitLab'
|
||||
|
||||
field :echo, GraphQL::STRING_TYPE, null: false, function: Functions::Echo.new
|
||||
end
|
||||
|
|
|
@ -277,7 +277,7 @@ module IssuablesHelper
|
|||
initialTaskStatus: issuable.task_status
|
||||
}
|
||||
|
||||
data[:hasClosingMergeRequest] = issuable.merge_requests_count != 0 if issuable.is_a?(Issue)
|
||||
data[:hasClosingMergeRequest] = issuable.merge_requests_count(current_user) != 0 if issuable.is_a?(Issue)
|
||||
|
||||
if parent.is_a?(Group)
|
||||
data[:groupPath] = parent.path
|
||||
|
|
|
@ -1,6 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module SnippetsHelper
|
||||
def snippets_upload_path(snippet, user)
|
||||
return unless user
|
||||
|
||||
if snippet&.persisted?
|
||||
upload_path('personal_snippet', id: snippet.id)
|
||||
else
|
||||
upload_path('user', id: user.id)
|
||||
end
|
||||
end
|
||||
|
||||
def reliable_snippet_path(snippet, opts = nil)
|
||||
if snippet.project_id?
|
||||
project_snippet_path(snippet.project, snippet, opts)
|
||||
|
|
|
@ -29,7 +29,11 @@ module Issuable
|
|||
# This object is used to gather issuable meta data for displaying
|
||||
# upvotes, downvotes, notes and closing merge requests count for issues and merge requests
|
||||
# lists avoiding n+1 queries and improving performance.
|
||||
IssuableMeta = Struct.new(:upvotes, :downvotes, :user_notes_count, :merge_requests_count)
|
||||
IssuableMeta = Struct.new(:upvotes, :downvotes, :user_notes_count, :mrs_count) do
|
||||
def merge_requests_count(user = nil)
|
||||
mrs_count
|
||||
end
|
||||
end
|
||||
|
||||
included do
|
||||
cache_markdown_field :title, pipeline: :single_line
|
||||
|
|
|
@ -270,8 +270,8 @@ class Issue < ApplicationRecord
|
|||
end
|
||||
# rubocop: enable CodeReuse/ServiceClass
|
||||
|
||||
def merge_requests_count
|
||||
merge_requests_closing_issues.count
|
||||
def merge_requests_count(user = nil)
|
||||
::MergeRequestsClosingIssues.count_for_issue(self.id, user)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -446,10 +446,10 @@ class Member < ApplicationRecord
|
|||
end
|
||||
|
||||
def higher_access_level_than_group
|
||||
if highest_group_member && highest_group_member.access_level >= access_level
|
||||
if highest_group_member && highest_group_member.access_level > access_level
|
||||
error_parameters = { access: highest_group_member.human_access, group_name: highest_group_member.group.name }
|
||||
|
||||
errors.add(:access_level, s_("should be higher than %{access} inherited membership from group %{group_name}") % error_parameters)
|
||||
errors.add(:access_level, s_("should be greater than or equal to %{access} inherited membership from group %{group_name}") % error_parameters)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,11 +7,38 @@ class MergeRequestsClosingIssues < ApplicationRecord
|
|||
validates :merge_request_id, uniqueness: { scope: :issue_id }, presence: true
|
||||
validates :issue_id, presence: true
|
||||
|
||||
scope :with_issues, ->(ids) { where(issue_id: ids) }
|
||||
scope :with_merge_requests_enabled, -> do
|
||||
joins(:merge_request)
|
||||
.joins('INNER JOIN project_features ON merge_requests.target_project_id = project_features.project_id')
|
||||
.where('project_features.merge_requests_access_level >= :access', access: ProjectFeature::ENABLED)
|
||||
end
|
||||
|
||||
scope :accessible_by, ->(user) do
|
||||
joins(:merge_request)
|
||||
.joins('INNER JOIN project_features ON merge_requests.target_project_id = project_features.project_id')
|
||||
.where('project_features.merge_requests_access_level >= :access OR EXISTS(:authorizations)',
|
||||
access: ProjectFeature::ENABLED,
|
||||
authorizations: user.authorizations_for_projects(min_access_level: Gitlab::Access::REPORTER, related_project_column: "merge_requests.target_project_id")
|
||||
)
|
||||
end
|
||||
|
||||
class << self
|
||||
def count_for_collection(ids)
|
||||
group(:issue_id)
|
||||
.where(issue_id: ids)
|
||||
.pluck('issue_id', 'COUNT(*) as count')
|
||||
def count_for_collection(ids, current_user)
|
||||
closing_merge_requests(ids, current_user).group(:issue_id).pluck('issue_id', 'COUNT(*) as count')
|
||||
end
|
||||
|
||||
def count_for_issue(id, current_user)
|
||||
closing_merge_requests(id, current_user).count
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def closing_merge_requests(ids, current_user)
|
||||
return with_issues(ids) if current_user&.admin?
|
||||
return with_issues(ids).with_merge_requests_enabled if current_user.blank?
|
||||
|
||||
with_issues(ids).accessible_by(current_user)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -461,10 +461,12 @@ class Project < ApplicationRecord
|
|||
|
||||
# Returns a collection of projects that is either public or visible to the
|
||||
# logged in user.
|
||||
def self.public_or_visible_to_user(user = nil)
|
||||
def self.public_or_visible_to_user(user = nil, min_access_level = nil)
|
||||
min_access_level = nil if user&.admin?
|
||||
|
||||
if user
|
||||
where('EXISTS (?) OR projects.visibility_level IN (?)',
|
||||
user.authorizations_for_projects,
|
||||
user.authorizations_for_projects(min_access_level: min_access_level),
|
||||
Gitlab::VisibilityLevel.levels_for_user(user))
|
||||
else
|
||||
public_to_user
|
||||
|
@ -474,30 +476,32 @@ class Project < ApplicationRecord
|
|||
# project features may be "disabled", "internal", "enabled" or "public". If "internal",
|
||||
# they are only available to team members. This scope returns projects where
|
||||
# the feature is either public, enabled, or internal with permission for the user.
|
||||
# Note: this scope doesn't enforce that the user has access to the projects, it just checks
|
||||
# that the user has access to the feature. It's important to use this scope with others
|
||||
# that checks project authorizations first.
|
||||
#
|
||||
# This method uses an optimised version of `with_feature_access_level` for
|
||||
# logged in users to more efficiently get private projects with the given
|
||||
# feature.
|
||||
def self.with_feature_available_for_user(feature, user)
|
||||
visible = [ProjectFeature::ENABLED, ProjectFeature::PUBLIC]
|
||||
min_access_level = ProjectFeature.required_minimum_access_level(feature)
|
||||
|
||||
if user&.admin?
|
||||
with_feature_enabled(feature)
|
||||
elsif user
|
||||
min_access_level = ProjectFeature.required_minimum_access_level(feature)
|
||||
column = ProjectFeature.quoted_access_level_column(feature)
|
||||
|
||||
with_project_feature
|
||||
.where(
|
||||
"(projects.visibility_level > :private AND (#{column} IS NULL OR #{column} >= (:public_visible) OR (#{column} = :private_visible AND EXISTS(:authorizations))))"\
|
||||
" OR (projects.visibility_level = :private AND (#{column} IS NULL OR #{column} >= :private_visible) AND EXISTS(:authorizations))",
|
||||
{
|
||||
private: Gitlab::VisibilityLevel::PRIVATE,
|
||||
public_visible: ProjectFeature::ENABLED,
|
||||
private_visible: ProjectFeature::PRIVATE,
|
||||
authorizations: user.authorizations_for_projects(min_access_level: min_access_level)
|
||||
})
|
||||
.where("#{column} IS NULL OR #{column} IN (:public_visible) OR (#{column} = :private_visible AND EXISTS (:authorizations))",
|
||||
{
|
||||
public_visible: visible,
|
||||
private_visible: ProjectFeature::PRIVATE,
|
||||
authorizations: user.authorizations_for_projects(min_access_level: min_access_level)
|
||||
})
|
||||
else
|
||||
# This has to be added to include features whose value is nil in the db
|
||||
visible << nil
|
||||
with_feature_access_level(feature, visible)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -761,11 +761,15 @@ class User < ApplicationRecord
|
|||
|
||||
# Typically used in conjunction with projects table to get projects
|
||||
# a user has been given access to.
|
||||
# The param `related_project_column` is the column to compare to the
|
||||
# project_authorizations. By default is projects.id
|
||||
#
|
||||
# Example use:
|
||||
# `Project.where('EXISTS(?)', user.authorizations_for_projects)`
|
||||
def authorizations_for_projects(min_access_level: nil)
|
||||
authorizations = project_authorizations.select(1).where('project_authorizations.project_id = projects.id')
|
||||
def authorizations_for_projects(min_access_level: nil, related_project_column: 'projects.id')
|
||||
authorizations = project_authorizations
|
||||
.select(1)
|
||||
.where("project_authorizations.project_id = #{related_project_column}")
|
||||
|
||||
return authorizations unless min_access_level.present?
|
||||
|
||||
|
|
5
app/policies/repository_policy.rb
Normal file
5
app/policies/repository_policy.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RepositoryPolicy < BasePolicy
|
||||
delegate { @subject.project }
|
||||
end
|
|
@ -4,7 +4,6 @@ module Ci
|
|||
class BuildRunnerPresenter < SimpleDelegator
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
DEFAULT_GIT_DEPTH_MERGE_REQUEST = 10
|
||||
RUNNER_REMOTE_TAG_PREFIX = 'refs/tags/'.freeze
|
||||
RUNNER_REMOTE_BRANCH_PREFIX = 'refs/remotes/origin/'.freeze
|
||||
|
||||
|
@ -28,7 +27,6 @@ module Ci
|
|||
def git_depth
|
||||
strong_memoize(:git_depth) do
|
||||
git_depth = variables&.find { |variable| variable[:key] == 'GIT_DEPTH' }&.dig(:value)
|
||||
git_depth ||= DEFAULT_GIT_DEPTH_MERGE_REQUEST if merge_request_ref?
|
||||
git_depth.to_i
|
||||
end
|
||||
end
|
||||
|
@ -39,12 +37,13 @@ module Ci
|
|||
if git_depth > 0
|
||||
specs << refspec_for_branch(ref) if branch? || legacy_detached_merge_request_pipeline?
|
||||
specs << refspec_for_tag(ref) if tag?
|
||||
specs << refspec_for_merge_request_ref if merge_request_ref?
|
||||
else
|
||||
specs << refspec_for_branch
|
||||
specs << refspec_for_tag
|
||||
end
|
||||
|
||||
specs << refspec_for_merge_request_ref if merge_request_ref?
|
||||
|
||||
specs
|
||||
end
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ module Lfs
|
|||
|
||||
def new_file(file_path, file_content, encoding: nil)
|
||||
if project.lfs_enabled? && lfs_file?(file_path)
|
||||
file_content = Base64.decode64(file_content) if encoding == 'base64'
|
||||
file_content = parse_file_content(file_content, encoding: encoding)
|
||||
lfs_pointer_file = Gitlab::Git::LfsPointerFile.new(file_content)
|
||||
lfs_object = create_lfs_object!(lfs_pointer_file, file_content)
|
||||
|
||||
|
@ -66,5 +66,12 @@ module Lfs
|
|||
def link_lfs_object!(lfs_object)
|
||||
project.lfs_objects << lfs_object
|
||||
end
|
||||
|
||||
def parse_file_content(file_content, encoding: nil)
|
||||
return file_content.read if file_content.respond_to?(:read)
|
||||
return Base64.decode64(file_content) if encoding == 'base64'
|
||||
|
||||
file_content
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,7 +9,7 @@ module Projects
|
|||
end
|
||||
|
||||
def execute
|
||||
Projects::HousekeepingService.new(@project, :gc).execute do
|
||||
Projects::HousekeepingService.new(@project).execute do
|
||||
repository.delete_all_refs_except(RESERVED_REF_PREFIXES)
|
||||
end
|
||||
rescue Projects::HousekeepingService::LeaseTaken => e
|
||||
|
|
|
@ -24,7 +24,7 @@ module Projects
|
|||
|
||||
def propagate_projects_with_template
|
||||
loop do
|
||||
batch = project_ids_batch
|
||||
batch = Project.uncached { project_ids_batch }
|
||||
|
||||
bulk_create_from_template(batch) unless batch.empty?
|
||||
|
||||
|
|
|
@ -1,22 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class FileMover
|
||||
attr_reader :secret, :file_name, :model, :update_field
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
def initialize(file_path, model, update_field = :description)
|
||||
attr_reader :secret, :file_name, :from_model, :to_model, :update_field
|
||||
|
||||
def initialize(file_path, update_field = :description, from_model:, to_model:)
|
||||
@secret = File.split(File.dirname(file_path)).last
|
||||
@file_name = File.basename(file_path)
|
||||
@model = model
|
||||
@from_model = from_model
|
||||
@to_model = to_model
|
||||
@update_field = update_field
|
||||
end
|
||||
|
||||
def execute
|
||||
temp_file_uploader.retrieve_from_store!(file_name)
|
||||
|
||||
return unless valid?
|
||||
|
||||
uploader.retrieve_from_store!(file_name)
|
||||
|
||||
move
|
||||
|
||||
if update_markdown
|
||||
uploader.record_upload
|
||||
update_upload_model
|
||||
uploader.schedule_background_upload
|
||||
end
|
||||
end
|
||||
|
@ -24,52 +31,77 @@ class FileMover
|
|||
private
|
||||
|
||||
def valid?
|
||||
Pathname.new(temp_file_path).realpath.to_path.start_with?(
|
||||
(Pathname(temp_file_uploader.root) + temp_file_uploader.base_dir).to_path
|
||||
)
|
||||
if temp_file_uploader.file_storage?
|
||||
Pathname.new(temp_file_path).realpath.to_path.start_with?(
|
||||
(Pathname(temp_file_uploader.root) + temp_file_uploader.base_dir).to_path
|
||||
)
|
||||
else
|
||||
temp_file_uploader.exists?
|
||||
end
|
||||
end
|
||||
|
||||
def move
|
||||
FileUtils.mkdir_p(File.dirname(file_path))
|
||||
FileUtils.move(temp_file_path, file_path)
|
||||
if temp_file_uploader.file_storage?
|
||||
FileUtils.mkdir_p(File.dirname(file_path))
|
||||
FileUtils.move(temp_file_path, file_path)
|
||||
else
|
||||
uploader.copy_file(temp_file_uploader.file)
|
||||
temp_file_uploader.upload.destroy!
|
||||
end
|
||||
end
|
||||
|
||||
def update_markdown
|
||||
updated_text = model.read_attribute(update_field)
|
||||
.gsub(temp_file_uploader.markdown_link, uploader.markdown_link)
|
||||
model.update_attribute(update_field, updated_text)
|
||||
updated_text = to_model.read_attribute(update_field)
|
||||
.gsub(temp_file_uploader.markdown_link, uploader.markdown_link)
|
||||
to_model.update_attribute(update_field, updated_text)
|
||||
rescue
|
||||
revert
|
||||
false
|
||||
end
|
||||
|
||||
def update_upload_model
|
||||
return unless upload = temp_file_uploader.upload
|
||||
return if upload.destroyed?
|
||||
|
||||
upload.update!(model: to_model)
|
||||
end
|
||||
|
||||
def temp_file_path
|
||||
return @temp_file_path if @temp_file_path
|
||||
|
||||
temp_file_uploader.retrieve_from_store!(file_name)
|
||||
|
||||
@temp_file_path = temp_file_uploader.file.path
|
||||
strong_memoize(:temp_file_path) do
|
||||
temp_file_uploader.file.path
|
||||
end
|
||||
end
|
||||
|
||||
def file_path
|
||||
return @file_path if @file_path
|
||||
|
||||
uploader.retrieve_from_store!(file_name)
|
||||
|
||||
@file_path = uploader.file.path
|
||||
strong_memoize(:file_path) do
|
||||
uploader.file.path
|
||||
end
|
||||
end
|
||||
|
||||
def uploader
|
||||
@uploader ||= PersonalFileUploader.new(model, secret: secret)
|
||||
@uploader ||=
|
||||
begin
|
||||
uploader = PersonalFileUploader.new(to_model, secret: secret)
|
||||
|
||||
# Enforcing a REMOTE object storage given FileUploader#retrieve_from_store! won't do it
|
||||
# (there's no upload at the target yet).
|
||||
if uploader.class.object_store_enabled?
|
||||
uploader.object_store = ::ObjectStorage::Store::REMOTE
|
||||
end
|
||||
|
||||
uploader
|
||||
end
|
||||
end
|
||||
|
||||
def temp_file_uploader
|
||||
@temp_file_uploader ||= PersonalFileUploader.new(nil, secret: secret)
|
||||
@temp_file_uploader ||= PersonalFileUploader.new(from_model, secret: secret)
|
||||
end
|
||||
|
||||
def revert
|
||||
Rails.logger.warn("Markdown not updated, file move reverted for #{model}")
|
||||
Rails.logger.warn("Markdown not updated, file move reverted for #{to_model}")
|
||||
|
||||
FileUtils.move(file_path, temp_file_path)
|
||||
if temp_file_uploader.file_storage?
|
||||
FileUtils.move(file_path, temp_file_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
# end
|
||||
#
|
||||
class ColorValidator < ActiveModel::EachValidator
|
||||
PATTERN = /\A\#[0-9A-Fa-f]{3}{1,2}+\Z/.freeze
|
||||
PATTERN = /\A\#(?:[0-9A-Fa-f]{3}){1,2}\Z/.freeze
|
||||
|
||||
def validate_each(record, attribute, value)
|
||||
unless value =~ PATTERN
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
- header_title _("Snippets"), snippets_path
|
||||
- snippets_upload_path = snippets_upload_path(@snippet, current_user)
|
||||
|
||||
- content_for :page_specific_javascripts do
|
||||
- if @snippet && current_user
|
||||
- if snippets_upload_path
|
||||
-# haml-lint:disable InlineJavaScript
|
||||
:javascript
|
||||
window.uploads_path = "#{upload_path('personal_snippet', id: @snippet.id)}";
|
||||
window.uploads_path = "#{snippets_upload_path}";
|
||||
|
||||
= render template: "layouts/application"
|
||||
|
|
|
@ -77,7 +77,7 @@
|
|||
|
||||
= edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue-edited-ago js-issue-edited-ago')
|
||||
|
||||
#js-related-merge-requests{ data: { endpoint: expose_url(api_v4_projects_issues_related_merge_requests_path(id: @project.id, issue_iid: @issue.iid)), project_namespace: @project.namespace.path, project_path: @project.path } }
|
||||
#js-related-merge-requests{ data: { endpoint: expose_path(api_v4_projects_issues_related_merge_requests_path(id: @project.id, issue_iid: @issue.iid)), project_namespace: @project.namespace.path, project_path: @project.path } }
|
||||
|
||||
- if can?(current_user, :download_code, @project)
|
||||
#related-branches{ data: { url: related_branches_project_issue_path(@project, @issue) } }
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
- issue_votes = @issuable_meta_data[issuable.id]
|
||||
- upvotes, downvotes = issue_votes.upvotes, issue_votes.downvotes
|
||||
- issuable_url = @collection_type == "Issue" ? issue_path(issuable, anchor: 'notes') : merge_request_path(issuable, anchor: 'notes')
|
||||
- issuable_mr = @issuable_meta_data[issuable.id].merge_requests_count
|
||||
- issuable_mr = @issuable_meta_data[issuable.id].merge_requests_count(current_user)
|
||||
|
||||
- if issuable_mr > 0
|
||||
%li.issuable-mr.d-none.d-sm-block.has-tooltip{ title: _('Related merge requests') }
|
||||
|
|
|
@ -41,7 +41,10 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
|
|||
#
|
||||
# Templates
|
||||
#
|
||||
get '/templates/:template_type/:key' => 'templates#show', as: :template, constraints: { key: %r{[^/]+} }
|
||||
get '/templates/:template_type/:key' => 'templates#show',
|
||||
as: :template,
|
||||
defaults: { format: 'json' },
|
||||
constraints: { key: %r{[^/]+}, template_type: /issue|merge_request/, format: 'json' }
|
||||
|
||||
resource :avatar, only: [:show, :destroy]
|
||||
resources :commit, only: [:show], constraints: { id: /\h{7,40}/ } do
|
||||
|
|
|
@ -7,7 +7,7 @@ scope path: :uploads do
|
|||
# show uploads for models, snippets (notes) available for now
|
||||
get '-/system/:model/:id/:secret/:filename',
|
||||
to: 'uploads#show',
|
||||
constraints: { model: /personal_snippet/, id: /\d+/, filename: %r{[^/]+} }
|
||||
constraints: { model: /personal_snippet|user/, id: /\d+/, filename: %r{[^/]+} }
|
||||
|
||||
# show temporary uploads
|
||||
get '-/system/temp/:secret/:filename',
|
||||
|
@ -28,7 +28,7 @@ scope path: :uploads do
|
|||
# create uploads for models, snippets (notes) available for now
|
||||
post ':model',
|
||||
to: 'uploads#create',
|
||||
constraints: { model: /personal_snippet/, id: /\d+/ },
|
||||
constraints: { model: /personal_snippet|user/, id: /\d+/ },
|
||||
as: 'upload'
|
||||
end
|
||||
|
||||
|
|
|
@ -493,9 +493,9 @@ module API
|
|||
expose :state, :created_at, :updated_at
|
||||
|
||||
# Avoids an N+1 query when metadata is included
|
||||
def issuable_metadata(subject, options, method)
|
||||
def issuable_metadata(subject, options, method, args = nil)
|
||||
cached_subject = options.dig(:issuable_metadata, subject.id)
|
||||
(cached_subject || subject).public_send(method) # rubocop: disable GitlabSecurity/PublicSend
|
||||
(cached_subject || subject).public_send(method, *args) # rubocop: disable GitlabSecurity/PublicSend
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -554,7 +554,7 @@ module API
|
|||
end
|
||||
|
||||
expose(:user_notes_count) { |issue, options| issuable_metadata(issue, options, :user_notes_count) }
|
||||
expose(:merge_requests_count) { |issue, options| issuable_metadata(issue, options, :merge_requests_count) }
|
||||
expose(:merge_requests_count) { |issue, options| issuable_metadata(issue, options, :merge_requests_count, options[:current_user]) }
|
||||
expose(:upvotes) { |issue, options| issuable_metadata(issue, options, :upvotes) }
|
||||
expose(:downvotes) { |issue, options| issuable_metadata(issue, options, :downvotes) }
|
||||
expose :due_date
|
||||
|
@ -731,7 +731,9 @@ module API
|
|||
merge_request.metrics&.pipeline
|
||||
end
|
||||
|
||||
expose :head_pipeline, using: 'API::Entities::Pipeline'
|
||||
expose :head_pipeline, using: 'API::Entities::Pipeline', if: -> (_, options) do
|
||||
Ability.allowed?(options[:current_user], :read_pipeline, options[:project])
|
||||
end
|
||||
|
||||
expose :diff_refs, using: Entities::DiffRefs
|
||||
|
||||
|
|
|
@ -13,6 +13,10 @@ module API
|
|||
available?(:merge_requests, project, options[:current_user])
|
||||
end
|
||||
|
||||
def expose_path(path)
|
||||
Gitlab::Utils.append_path(Gitlab.config.gitlab.relative_url_root, path)
|
||||
end
|
||||
|
||||
def expose_url(path)
|
||||
url_options = Gitlab::Application.routes.default_url_options
|
||||
protocol, host, port, script_name = url_options.values_at(:protocol, :host, :port, :script_name)
|
||||
|
|
|
@ -93,7 +93,7 @@ module API
|
|||
options = {
|
||||
with: Entities::IssueBasic,
|
||||
current_user: current_user,
|
||||
issuable_metadata: issuable_meta_data(issues, 'Issue')
|
||||
issuable_metadata: issuable_meta_data(issues, 'Issue', current_user)
|
||||
}
|
||||
|
||||
present issues, options
|
||||
|
@ -120,7 +120,7 @@ module API
|
|||
options = {
|
||||
with: Entities::IssueBasic,
|
||||
current_user: current_user,
|
||||
issuable_metadata: issuable_meta_data(issues, 'Issue')
|
||||
issuable_metadata: issuable_meta_data(issues, 'Issue', current_user)
|
||||
}
|
||||
|
||||
present issues, options
|
||||
|
@ -150,7 +150,7 @@ module API
|
|||
with: Entities::IssueBasic,
|
||||
current_user: current_user,
|
||||
project: user_project,
|
||||
issuable_metadata: issuable_meta_data(issues, 'Issue')
|
||||
issuable_metadata: issuable_meta_data(issues, 'Issue', current_user)
|
||||
}
|
||||
|
||||
present issues, options
|
||||
|
|
|
@ -71,7 +71,7 @@ module API
|
|||
if params[:view] == 'simple'
|
||||
options[:with] = Entities::MergeRequestSimple
|
||||
else
|
||||
options[:issuable_metadata] = issuable_meta_data(merge_requests, 'MergeRequest')
|
||||
options[:issuable_metadata] = issuable_meta_data(merge_requests, 'MergeRequest', current_user)
|
||||
end
|
||||
|
||||
options
|
||||
|
|
|
@ -65,7 +65,7 @@ module API
|
|||
next unless collection
|
||||
|
||||
targets = collection.map(&:target)
|
||||
options[type] = { issuable_metadata: issuable_meta_data(targets, type) }
|
||||
options[type] = { issuable_metadata: issuable_meta_data(targets, type, current_user) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -100,7 +100,7 @@ module Banzai
|
|||
end
|
||||
|
||||
def relative_file_path(uri)
|
||||
path = Addressable::URI.unescape(uri.path)
|
||||
path = Addressable::URI.unescape(uri.path).delete("\0")
|
||||
request_path = Addressable::URI.unescape(context[:requested_path])
|
||||
nested_path = build_relative_path(path, request_path)
|
||||
file_exists?(nested_path) ? nested_path : path
|
||||
|
|
|
@ -15,6 +15,9 @@ module Gitlab
|
|||
|
||||
@global = Entry::Global.new(@config)
|
||||
@global.compose!
|
||||
rescue Gitlab::Config::Loader::Yaml::DataTooLargeError => e
|
||||
Gitlab::Sentry.track_exception(e, extra: { user: user.inspect, project: project.inspect })
|
||||
raise Config::ConfigError, e.message
|
||||
rescue Gitlab::Config::Loader::FormatError,
|
||||
Extendable::ExtensionError,
|
||||
External::Processor::IncludeError => e
|
||||
|
|
|
@ -4,6 +4,13 @@ module Gitlab
|
|||
module Config
|
||||
module Loader
|
||||
class Yaml
|
||||
DataTooLargeError = Class.new(Loader::FormatError)
|
||||
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
MAX_YAML_SIZE = 1.megabyte
|
||||
MAX_YAML_DEPTH = 100
|
||||
|
||||
def initialize(config)
|
||||
@config = YAML.safe_load(config, [Symbol], [], true)
|
||||
rescue Psych::Exception => e
|
||||
|
@ -11,16 +18,35 @@ module Gitlab
|
|||
end
|
||||
|
||||
def valid?
|
||||
@config.is_a?(Hash)
|
||||
hash? && !too_big?
|
||||
end
|
||||
|
||||
def load!
|
||||
unless valid?
|
||||
raise Loader::FormatError, 'Invalid configuration format'
|
||||
end
|
||||
raise DataTooLargeError, 'The parsed YAML is too big' if too_big?
|
||||
raise Loader::FormatError, 'Invalid configuration format' unless hash?
|
||||
|
||||
@config.deep_symbolize_keys
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def hash?
|
||||
@config.is_a?(Hash)
|
||||
end
|
||||
|
||||
def too_big?
|
||||
return false unless Feature.enabled?(:ci_yaml_limit_size, default_enabled: true)
|
||||
|
||||
!deep_size.valid?
|
||||
end
|
||||
|
||||
def deep_size
|
||||
strong_memoize(:deep_size) do
|
||||
Gitlab::Utils::DeepSize.new(@config,
|
||||
max_size: MAX_YAML_SIZE,
|
||||
max_depth: MAX_YAML_DEPTH)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -19,7 +19,7 @@ module Gitlab
|
|||
def hook_attrs(pipeline)
|
||||
{
|
||||
id: pipeline.id,
|
||||
ref: pipeline.ref,
|
||||
ref: pipeline.source_ref,
|
||||
tag: pipeline.tag,
|
||||
sha: pipeline.sha,
|
||||
before_sha: pipeline.before_sha,
|
||||
|
|
|
@ -905,6 +905,12 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
def remove_foreign_key_if_exists(*args)
|
||||
if foreign_key_exists?(*args)
|
||||
remove_foreign_key(*args)
|
||||
end
|
||||
end
|
||||
|
||||
def remove_foreign_key_without_error(*args)
|
||||
remove_foreign_key(*args)
|
||||
rescue ArgumentError
|
||||
|
|
|
@ -303,6 +303,11 @@ module Gitlab
|
|||
(size.to_f / 1024).round(2)
|
||||
end
|
||||
|
||||
# Return git object directory size in bytes
|
||||
def object_directory_size
|
||||
gitaly_repository_client.get_object_directory_size.to_f * 1024
|
||||
end
|
||||
|
||||
# Build an array of commits.
|
||||
#
|
||||
# Usage.
|
||||
|
|
|
@ -43,6 +43,8 @@ module Gitlab
|
|||
ordered_entries.concat(tree_entries_from_rugged(repository, sha, entry.path, true))
|
||||
end
|
||||
end
|
||||
|
||||
ordered_entries
|
||||
end
|
||||
|
||||
def rugged_populate_flat_path(repository, sha, path, entries)
|
||||
|
|
|
@ -47,6 +47,13 @@ module Gitlab
|
|||
response.size
|
||||
end
|
||||
|
||||
def get_object_directory_size
|
||||
request = Gitaly::GetObjectDirectorySizeRequest.new(repository: @gitaly_repo)
|
||||
response = GitalyClient.call(@storage, :repository_service, :get_object_directory_size, request, timeout: GitalyClient.medium_timeout)
|
||||
|
||||
response.size
|
||||
end
|
||||
|
||||
def apply_gitattributes(revision)
|
||||
request = Gitaly::ApplyGitattributesRequest.new(repository: @gitaly_repo, revision: encode_binary(revision))
|
||||
GitalyClient.call(@storage, :repository_service, :apply_gitattributes, request, timeout: GitalyClient.fast_timeout)
|
||||
|
|
|
@ -39,6 +39,8 @@ module Gitlab
|
|||
type = node_type_for_basic_connection(type)
|
||||
end
|
||||
|
||||
type = type.unwrap if type.kind.non_null?
|
||||
|
||||
Array.wrap(type.metadata[:authorize])
|
||||
end
|
||||
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
module Gitlab
|
||||
class GroupSearchResults < SearchResults
|
||||
attr_reader :group
|
||||
|
||||
def initialize(current_user, limit_projects, group, query, default_project_filter: false, per_page: 20)
|
||||
super(current_user, limit_projects, query, default_project_filter: default_project_filter, per_page: per_page)
|
||||
|
||||
|
@ -26,5 +28,9 @@ module Gitlab
|
|||
.where(id: groups.select('members.user_id'))
|
||||
end
|
||||
# rubocop:enable CodeReuse/ActiveRecord
|
||||
|
||||
def issuable_params
|
||||
super.merge(group_id: group.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
module Gitlab
|
||||
module IssuableMetadata
|
||||
def issuable_meta_data(issuable_collection, collection_type)
|
||||
def issuable_meta_data(issuable_collection, collection_type, user = nil)
|
||||
# ActiveRecord uses Object#extend for null relations.
|
||||
if !(issuable_collection.singleton_class < ActiveRecord::NullRelation) &&
|
||||
issuable_collection.respond_to?(:limit_value) &&
|
||||
|
@ -23,7 +23,7 @@ module Gitlab
|
|||
issuable_votes_count = ::AwardEmoji.votes_for_collection(issuable_ids, collection_type)
|
||||
issuable_merge_requests_count =
|
||||
if collection_type == 'Issue'
|
||||
::MergeRequestsClosingIssues.count_for_collection(issuable_ids)
|
||||
::MergeRequestsClosingIssues.count_for_collection(issuable_ids, user)
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
|
|
@ -151,5 +151,9 @@ module Gitlab
|
|||
def repository_wiki_ref
|
||||
@repository_wiki_ref ||= repository_ref || project.wiki.default_branch
|
||||
end
|
||||
|
||||
def issuable_params
|
||||
super.merge(project_id: project.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
module Gitlab
|
||||
class SearchResults
|
||||
COUNT_LIMIT = 1001
|
||||
|
||||
attr_reader :current_user, :query, :per_page
|
||||
|
||||
# Limit search results by passed projects
|
||||
|
@ -25,29 +27,26 @@ module Gitlab
|
|||
def objects(scope, page = nil, without_count = true)
|
||||
collection = case scope
|
||||
when 'projects'
|
||||
projects.page(page).per(per_page)
|
||||
projects
|
||||
when 'issues'
|
||||
issues.page(page).per(per_page)
|
||||
issues
|
||||
when 'merge_requests'
|
||||
merge_requests.page(page).per(per_page)
|
||||
merge_requests
|
||||
when 'milestones'
|
||||
milestones.page(page).per(per_page)
|
||||
milestones
|
||||
when 'users'
|
||||
users.page(page).per(per_page)
|
||||
users
|
||||
else
|
||||
Kaminari.paginate_array([]).page(page).per(per_page)
|
||||
end
|
||||
Kaminari.paginate_array([])
|
||||
end.page(page).per(per_page)
|
||||
|
||||
without_count ? collection.without_count : collection
|
||||
end
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def limited_projects_count
|
||||
@limited_projects_count ||= projects.limit(count_limit).count
|
||||
@limited_projects_count ||= limited_count(projects)
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def limited_issues_count
|
||||
return @limited_issues_count if @limited_issues_count
|
||||
|
||||
|
@ -56,35 +55,28 @@ module Gitlab
|
|||
# and confidential issues user has access to, is too complex.
|
||||
# It's faster to try to fetch all public issues first, then only
|
||||
# if necessary try to fetch all issues.
|
||||
sum = issues(public_only: true).limit(count_limit).count
|
||||
@limited_issues_count = sum < count_limit ? issues.limit(count_limit).count : sum
|
||||
sum = limited_count(issues(public_only: true))
|
||||
@limited_issues_count = sum < count_limit ? limited_count(issues) : sum
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def limited_merge_requests_count
|
||||
@limited_merge_requests_count ||= merge_requests.limit(count_limit).count
|
||||
@limited_merge_requests_count ||= limited_count(merge_requests)
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def limited_milestones_count
|
||||
@limited_milestones_count ||= milestones.limit(count_limit).count
|
||||
@limited_milestones_count ||= limited_count(milestones)
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
# rubocop:disable CodeReuse/ActiveRecord
|
||||
def limited_users_count
|
||||
@limited_users_count ||= users.limit(count_limit).count
|
||||
@limited_users_count ||= limited_count(users)
|
||||
end
|
||||
# rubocop:enable CodeReuse/ActiveRecord
|
||||
|
||||
def single_commit_result?
|
||||
false
|
||||
end
|
||||
|
||||
def count_limit
|
||||
1001
|
||||
COUNT_LIMIT
|
||||
end
|
||||
|
||||
def users
|
||||
|
@ -99,23 +91,15 @@ module Gitlab
|
|||
limit_projects.search(query)
|
||||
end
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def issues(finder_params = {})
|
||||
issues = IssuesFinder.new(current_user, finder_params).execute
|
||||
issues = IssuesFinder.new(current_user, issuable_params.merge(finder_params)).execute
|
||||
|
||||
unless default_project_filter
|
||||
issues = issues.where(project_id: project_ids_relation)
|
||||
issues = issues.where(project_id: project_ids_relation) # rubocop: disable CodeReuse/ActiveRecord
|
||||
end
|
||||
|
||||
issues =
|
||||
if query =~ /#(\d+)\z/
|
||||
issues.where(iid: $1)
|
||||
else
|
||||
issues.full_search(query)
|
||||
end
|
||||
|
||||
issues.reorder('updated_at DESC')
|
||||
issues
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def milestones
|
||||
|
@ -127,23 +111,15 @@ module Gitlab
|
|||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def merge_requests
|
||||
merge_requests = MergeRequestsFinder.new(current_user).execute
|
||||
merge_requests = MergeRequestsFinder.new(current_user, issuable_params).execute
|
||||
|
||||
unless default_project_filter
|
||||
merge_requests = merge_requests.in_projects(project_ids_relation)
|
||||
end
|
||||
|
||||
merge_requests =
|
||||
if query =~ /[#!](\d+)\z/
|
||||
merge_requests.where(iid: $1)
|
||||
else
|
||||
merge_requests.full_search(query)
|
||||
end
|
||||
|
||||
merge_requests.reorder('updated_at DESC')
|
||||
merge_requests
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
def default_scope
|
||||
'projects'
|
||||
|
@ -174,5 +150,23 @@ module Gitlab
|
|||
limit_projects.select(:id).reorder(nil)
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
def issuable_params
|
||||
{}.tap do |params|
|
||||
params[:sort] = 'updated_desc'
|
||||
|
||||
if query =~ /#(\d+)\z/
|
||||
params[:iids] = $1
|
||||
else
|
||||
params[:search] = query
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def limited_count(relation)
|
||||
relation.reorder(nil).limit(count_limit).size
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
end
|
||||
end
|
||||
|
|
79
lib/gitlab/utils/deep_size.rb
Normal file
79
lib/gitlab/utils/deep_size.rb
Normal file
|
@ -0,0 +1,79 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'objspace'
|
||||
|
||||
module Gitlab
|
||||
module Utils
|
||||
class DeepSize
|
||||
Error = Class.new(StandardError)
|
||||
TooMuchDataError = Class.new(Error)
|
||||
|
||||
DEFAULT_MAX_SIZE = 1.megabyte
|
||||
DEFAULT_MAX_DEPTH = 100
|
||||
|
||||
def initialize(root, max_size: DEFAULT_MAX_SIZE, max_depth: DEFAULT_MAX_DEPTH)
|
||||
@root = root
|
||||
@max_size = max_size
|
||||
@max_depth = max_depth
|
||||
@size = 0
|
||||
@depth = 0
|
||||
|
||||
evaluate
|
||||
end
|
||||
|
||||
def valid?
|
||||
!too_big? && !too_deep?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def evaluate
|
||||
add_object(@root)
|
||||
rescue Error
|
||||
# NOOP
|
||||
end
|
||||
|
||||
def too_big?
|
||||
@size > @max_size
|
||||
end
|
||||
|
||||
def too_deep?
|
||||
@depth > @max_depth
|
||||
end
|
||||
|
||||
def add_object(object)
|
||||
@size += ObjectSpace.memsize_of(object)
|
||||
raise TooMuchDataError if @size > @max_size
|
||||
|
||||
add_array(object) if object.is_a?(Array)
|
||||
add_hash(object) if object.is_a?(Hash)
|
||||
end
|
||||
|
||||
def add_array(object)
|
||||
with_nesting do
|
||||
object.each do |n|
|
||||
add_object(n)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def add_hash(object)
|
||||
with_nesting do
|
||||
object.each do |key, value|
|
||||
add_object(key)
|
||||
add_object(value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def with_nesting
|
||||
@depth += 1
|
||||
raise TooMuchDataError if too_deep?
|
||||
|
||||
yield
|
||||
|
||||
@depth -= 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -10281,7 +10281,7 @@ msgstr[1] ""
|
|||
msgid "score"
|
||||
msgstr ""
|
||||
|
||||
msgid "should be higher than %{access} inherited membership from group %{group_name}"
|
||||
msgid "should be greater than or equal to %{access} inherited membership from group %{group_name}"
|
||||
msgstr ""
|
||||
|
||||
msgid "show less"
|
||||
|
|
|
@ -250,7 +250,7 @@ describe Projects::NotesController do
|
|||
before do
|
||||
service_params = ActionController::Parameters.new({
|
||||
note: 'some note',
|
||||
noteable_id: merge_request.id.to_s,
|
||||
noteable_id: merge_request.id,
|
||||
noteable_type: 'MergeRequest',
|
||||
commit_id: nil,
|
||||
merge_request_diff_head_sha: 'sha'
|
||||
|
|
|
@ -1,49 +1,101 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Projects::TemplatesController do
|
||||
let(:project) { create(:project, :repository) }
|
||||
let(:project) { create(:project, :repository, :private) }
|
||||
let(:user) { create(:user) }
|
||||
let(:user2) { create(:user) }
|
||||
let(:file_path_1) { '.gitlab/issue_templates/bug.md' }
|
||||
let(:file_path_1) { '.gitlab/issue_templates/issue_template.md' }
|
||||
let(:file_path_2) { '.gitlab/merge_request_templates/merge_request_template.md' }
|
||||
let(:body) { JSON.parse(response.body) }
|
||||
|
||||
before do
|
||||
project.add_developer(user)
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
before do
|
||||
project.add_user(user, Gitlab::Access::MAINTAINER)
|
||||
project.repository.create_file(user, file_path_1, 'something valid',
|
||||
message: 'test 3', branch_name: 'master')
|
||||
end
|
||||
let!(:file_1) { project.repository.create_file(user, file_path_1, 'issue content', message: 'message', branch_name: 'master') }
|
||||
let!(:file_2) { project.repository.create_file(user, file_path_2, 'merge request content', message: 'message', branch_name: 'master') }
|
||||
|
||||
describe '#show' do
|
||||
it 'renders template name and content as json' do
|
||||
get(:show, params: { namespace_id: project.namespace.to_param, template_type: "issue", key: "bug", project_id: project }, format: :json)
|
||||
shared_examples 'renders issue templates as json' do
|
||||
it do
|
||||
get(:show, params: { namespace_id: project.namespace, template_type: 'issue', key: 'issue_template', project_id: project }, format: :json)
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
expect(body["name"]).to eq("bug")
|
||||
expect(body["content"]).to eq("something valid")
|
||||
expect(response.status).to eq(200)
|
||||
expect(body['name']).to eq('issue_template')
|
||||
expect(body['content']).to eq('issue content')
|
||||
end
|
||||
end
|
||||
|
||||
it 'renders 404 when unauthorized' do
|
||||
sign_in(user2)
|
||||
get(:show, params: { namespace_id: project.namespace.to_param, template_type: "issue", key: "bug", project_id: project }, format: :json)
|
||||
shared_examples 'renders merge request templates as json' do
|
||||
it do
|
||||
get(:show, params: { namespace_id: project.namespace, template_type: 'merge_request', key: 'merge_request_template', project_id: project }, format: :json)
|
||||
|
||||
expect(response.status).to eq(404)
|
||||
expect(response.status).to eq(200)
|
||||
expect(body['name']).to eq('merge_request_template')
|
||||
expect(body['content']).to eq('merge request content')
|
||||
end
|
||||
end
|
||||
|
||||
it 'renders 404 when template type is not found' do
|
||||
sign_in(user)
|
||||
get(:show, params: { namespace_id: project.namespace.to_param, template_type: "dont_exist", key: "bug", project_id: project }, format: :json)
|
||||
shared_examples 'renders 404 when requesting an issue template' do
|
||||
it do
|
||||
get(:show, params: { namespace_id: project.namespace, template_type: 'issue', key: 'issue_template', project_id: project }, format: :json)
|
||||
|
||||
expect(response.status).to eq(404)
|
||||
expect(response.status).to eq(404)
|
||||
end
|
||||
end
|
||||
|
||||
it 'renders 404 without errors' do
|
||||
sign_in(user)
|
||||
expect { get(:show, params: { namespace_id: project.namespace.to_param, template_type: "dont_exist", key: "bug", project_id: project }, format: :json) }.not_to raise_error
|
||||
shared_examples 'renders 404 when requesting a merge request template' do
|
||||
it do
|
||||
get(:show, params: { namespace_id: project.namespace, template_type: 'merge_request', key: 'merge_request_template', project_id: project }, format: :json)
|
||||
|
||||
expect(response.status).to eq(404)
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'renders 404 when params are invalid' do
|
||||
it 'does not route when the template type is invalid' do
|
||||
expect do
|
||||
get(:show, params: { namespace_id: project.namespace, template_type: 'invalid_type', key: 'issue_template', project_id: project }, format: :json)
|
||||
end.to raise_error(ActionController::UrlGenerationError)
|
||||
end
|
||||
|
||||
it 'renders 404 when the format type is invalid' do
|
||||
get(:show, params: { namespace_id: project.namespace, template_type: 'issue', key: 'issue_template', project_id: project }, format: :html)
|
||||
|
||||
expect(response.status).to eq(404)
|
||||
end
|
||||
|
||||
it 'renders 404 when the key is unknown' do
|
||||
get(:show, params: { namespace_id: project.namespace, template_type: 'issue', key: 'unknown_template', project_id: project }, format: :json)
|
||||
|
||||
expect(response.status).to eq(404)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the user is not a member of the project' do
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
include_examples 'renders 404 when requesting an issue template'
|
||||
include_examples 'renders 404 when requesting a merge request template'
|
||||
include_examples 'renders 404 when params are invalid'
|
||||
end
|
||||
|
||||
context 'when user is a member of the project' do
|
||||
before do
|
||||
project.add_developer(user)
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
include_examples 'renders issue templates as json'
|
||||
include_examples 'renders merge request templates as json'
|
||||
include_examples 'renders 404 when params are invalid'
|
||||
end
|
||||
|
||||
context 'when user is a guest of the project' do
|
||||
before do
|
||||
project.add_guest(user)
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
include_examples 'renders issue templates as json'
|
||||
include_examples 'renders 404 when requesting a merge request template'
|
||||
include_examples 'renders 404 when params are invalid'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -117,6 +117,119 @@ describe Snippets::NotesController do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'POST create' do
|
||||
context 'when a snippet is public' do
|
||||
let(:request_params) do
|
||||
{
|
||||
note: attributes_for(:note_on_personal_snippet, noteable: public_snippet),
|
||||
snippet_id: public_snippet.id
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
sign_in user
|
||||
end
|
||||
|
||||
it 'returns status 302' do
|
||||
post :create, params: request_params
|
||||
|
||||
expect(response).to have_gitlab_http_status(302)
|
||||
end
|
||||
|
||||
it 'creates the note' do
|
||||
expect { post :create, params: request_params }.to change { Note.count }.by(1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a snippet is internal' do
|
||||
let(:request_params) do
|
||||
{
|
||||
note: attributes_for(:note_on_personal_snippet, noteable: internal_snippet),
|
||||
snippet_id: internal_snippet.id
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
sign_in user
|
||||
end
|
||||
|
||||
it 'returns status 302' do
|
||||
post :create, params: request_params
|
||||
|
||||
expect(response).to have_gitlab_http_status(302)
|
||||
end
|
||||
|
||||
it 'creates the note' do
|
||||
expect { post :create, params: request_params }.to change { Note.count }.by(1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a snippet is private' do
|
||||
let(:request_params) do
|
||||
{
|
||||
note: attributes_for(:note_on_personal_snippet, noteable: private_snippet),
|
||||
snippet_id: private_snippet.id
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
sign_in user
|
||||
end
|
||||
|
||||
context 'when user is not the author' do
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
it 'returns status 404' do
|
||||
post :create, params: request_params
|
||||
|
||||
expect(response).to have_gitlab_http_status(404)
|
||||
end
|
||||
|
||||
it 'does not create the note' do
|
||||
expect { post :create, params: request_params }.not_to change { Note.count }
|
||||
end
|
||||
|
||||
context 'when user sends a snippet_id for a public snippet' do
|
||||
let(:request_params) do
|
||||
{
|
||||
note: attributes_for(:note_on_personal_snippet, noteable: private_snippet),
|
||||
snippet_id: public_snippet.id
|
||||
}
|
||||
end
|
||||
|
||||
it 'returns status 302' do
|
||||
post :create, params: request_params
|
||||
|
||||
expect(response).to have_gitlab_http_status(302)
|
||||
end
|
||||
|
||||
it 'creates the note on the public snippet' do
|
||||
expect { post :create, params: request_params }.to change { Note.count }.by(1)
|
||||
expect(Note.last.noteable).to eq public_snippet
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is the author' do
|
||||
before do
|
||||
sign_in(private_snippet.author)
|
||||
end
|
||||
|
||||
it 'returns status 302' do
|
||||
post :create, params: request_params
|
||||
|
||||
expect(response).to have_gitlab_http_status(302)
|
||||
end
|
||||
|
||||
it 'creates the note' do
|
||||
expect { post :create, params: request_params }.to change { Note.count }.by(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE destroy' do
|
||||
let(:request_params) do
|
||||
{
|
||||
|
|
|
@ -207,8 +207,8 @@ describe SnippetsController do
|
|||
context 'when the snippet description contains a file' do
|
||||
include FileMoverHelpers
|
||||
|
||||
let(:picture_file) { '/-/system/temp/secret56/picture.jpg' }
|
||||
let(:text_file) { '/-/system/temp/secret78/text.txt' }
|
||||
let(:picture_file) { "/-/system/user/#{user.id}/secret56/picture.jpg" }
|
||||
let(:text_file) { "/-/system/user/#{user.id}/secret78/text.txt" }
|
||||
let(:description) do
|
||||
"Description with picture: ![picture](/uploads#{picture_file}) and "\
|
||||
"text: [text.txt](/uploads#{text_file})"
|
||||
|
|
|
@ -22,121 +22,160 @@ describe UploadsController do
|
|||
let!(:user) { create(:user, avatar: fixture_file_upload("spec/fixtures/dk.png", "image/png")) }
|
||||
|
||||
describe 'POST create' do
|
||||
let(:model) { 'personal_snippet' }
|
||||
let(:snippet) { create(:personal_snippet, :public) }
|
||||
let(:jpg) { fixture_file_upload('spec/fixtures/rails_sample.jpg', 'image/jpg') }
|
||||
let(:txt) { fixture_file_upload('spec/fixtures/doc_sample.txt', 'text/plain') }
|
||||
|
||||
context 'when a user does not have permissions to upload a file' do
|
||||
it "returns 401 when the user is not logged in" do
|
||||
post :create, params: { model: model, id: snippet.id }, format: :json
|
||||
context 'snippet uploads' do
|
||||
let(:model) { 'personal_snippet' }
|
||||
let(:snippet) { create(:personal_snippet, :public) }
|
||||
|
||||
context 'when a user does not have permissions to upload a file' do
|
||||
it "returns 401 when the user is not logged in" do
|
||||
post :create, params: { model: model, id: snippet.id }, format: :json
|
||||
|
||||
expect(response).to have_gitlab_http_status(401)
|
||||
end
|
||||
|
||||
it "returns 404 when user can't comment on a snippet" do
|
||||
private_snippet = create(:personal_snippet, :private)
|
||||
|
||||
sign_in(user)
|
||||
post :create, params: { model: model, id: private_snippet.id }, format: :json
|
||||
|
||||
expect(response).to have_gitlab_http_status(404)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a user is logged in' do
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
it "returns an error without file" do
|
||||
post :create, params: { model: model, id: snippet.id }, format: :json
|
||||
|
||||
expect(response).to have_gitlab_http_status(422)
|
||||
end
|
||||
|
||||
it "returns an error with invalid model" do
|
||||
expect { post :create, params: { model: 'invalid', id: snippet.id }, format: :json }
|
||||
.to raise_error(ActionController::UrlGenerationError)
|
||||
end
|
||||
|
||||
it "returns 404 status when object not found" do
|
||||
post :create, params: { model: model, id: 9999 }, format: :json
|
||||
|
||||
expect(response).to have_gitlab_http_status(404)
|
||||
end
|
||||
|
||||
context 'with valid image' do
|
||||
before do
|
||||
post :create, params: { model: 'personal_snippet', id: snippet.id, file: jpg }, format: :json
|
||||
end
|
||||
|
||||
it 'returns a content with original filename, new link, and correct type.' do
|
||||
expect(response.body).to match '\"alt\":\"rails_sample\"'
|
||||
expect(response.body).to match "\"url\":\"/uploads"
|
||||
end
|
||||
|
||||
it 'creates a corresponding Upload record' do
|
||||
upload = Upload.last
|
||||
|
||||
aggregate_failures do
|
||||
expect(upload).to exist
|
||||
expect(upload.model).to eq snippet
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with valid non-image file' do
|
||||
before do
|
||||
post :create, params: { model: 'personal_snippet', id: snippet.id, file: txt }, format: :json
|
||||
end
|
||||
|
||||
it 'returns a content with original filename, new link, and correct type.' do
|
||||
expect(response.body).to match '\"alt\":\"doc_sample.txt\"'
|
||||
expect(response.body).to match "\"url\":\"/uploads"
|
||||
end
|
||||
|
||||
it 'creates a corresponding Upload record' do
|
||||
upload = Upload.last
|
||||
|
||||
aggregate_failures do
|
||||
expect(upload).to exist
|
||||
expect(upload.model).to eq snippet
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'user uploads' do
|
||||
let(:model) { 'user' }
|
||||
|
||||
it 'returns 401 when the user has no access' do
|
||||
post :create, params: { model: 'user', id: user.id }, format: :json
|
||||
|
||||
expect(response).to have_gitlab_http_status(401)
|
||||
end
|
||||
|
||||
it "returns 404 when user can't comment on a snippet" do
|
||||
private_snippet = create(:personal_snippet, :private)
|
||||
|
||||
sign_in(user)
|
||||
post :create, params: { model: model, id: private_snippet.id }, format: :json
|
||||
|
||||
expect(response).to have_gitlab_http_status(404)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a user is logged in' do
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
it "returns an error without file" do
|
||||
post :create, params: { model: model, id: snippet.id }, format: :json
|
||||
|
||||
expect(response).to have_gitlab_http_status(422)
|
||||
end
|
||||
|
||||
it "returns an error with invalid model" do
|
||||
expect { post :create, params: { model: 'invalid', id: snippet.id }, format: :json }
|
||||
.to raise_error(ActionController::UrlGenerationError)
|
||||
end
|
||||
|
||||
it "returns 404 status when object not found" do
|
||||
post :create, params: { model: model, id: 9999 }, format: :json
|
||||
|
||||
expect(response).to have_gitlab_http_status(404)
|
||||
end
|
||||
|
||||
context 'with valid image' do
|
||||
context 'when user is logged in' do
|
||||
before do
|
||||
post :create, params: { model: 'personal_snippet', id: snippet.id, file: jpg }, format: :json
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
it 'returns a content with original filename, new link, and correct type.' do
|
||||
expect(response.body).to match '\"alt\":\"rails_sample\"'
|
||||
expect(response.body).to match "\"url\":\"/uploads"
|
||||
end
|
||||
|
||||
it 'creates a corresponding Upload record' do
|
||||
upload = Upload.last
|
||||
|
||||
aggregate_failures do
|
||||
expect(upload).to exist
|
||||
expect(upload.model).to eq snippet
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with valid non-image file' do
|
||||
before do
|
||||
post :create, params: { model: 'personal_snippet', id: snippet.id, file: txt }, format: :json
|
||||
end
|
||||
|
||||
it 'returns a content with original filename, new link, and correct type.' do
|
||||
expect(response.body).to match '\"alt\":\"doc_sample.txt\"'
|
||||
expect(response.body).to match "\"url\":\"/uploads"
|
||||
end
|
||||
|
||||
it 'creates a corresponding Upload record' do
|
||||
upload = Upload.last
|
||||
|
||||
aggregate_failures do
|
||||
expect(upload).to exist
|
||||
expect(upload.model).to eq snippet
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'temporal with valid image' do
|
||||
subject do
|
||||
post :create, params: { model: 'personal_snippet', file: jpg }, format: :json
|
||||
post :create, params: { model: model, id: user.id, file: jpg }, format: :json
|
||||
end
|
||||
|
||||
it 'returns a content with original filename, new link, and correct type.' do
|
||||
subject
|
||||
|
||||
expect(response.body).to match '\"alt\":\"rails_sample\"'
|
||||
expect(response.body).to match "\"url\":\"/uploads/-/system/temp"
|
||||
expect(response.body).to match "\"url\":\"/uploads/-/system/user/#{user.id}/"
|
||||
end
|
||||
|
||||
it 'does not create an Upload record' do
|
||||
expect { subject }.not_to change { Upload.count }
|
||||
end
|
||||
end
|
||||
it 'creates a corresponding Upload record' do
|
||||
expect { subject }.to change { Upload.count }
|
||||
|
||||
context 'temporal with valid non-image file' do
|
||||
subject do
|
||||
post :create, params: { model: 'personal_snippet', file: txt }, format: :json
|
||||
upload = Upload.last
|
||||
|
||||
aggregate_failures do
|
||||
expect(upload).to exist
|
||||
expect(upload.model).to eq user
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns a content with original filename, new link, and correct type.' do
|
||||
subject
|
||||
context 'with valid non-image file' do
|
||||
subject do
|
||||
post :create, params: { model: model, id: user.id, file: txt }, format: :json
|
||||
end
|
||||
|
||||
expect(response.body).to match '\"alt\":\"doc_sample.txt\"'
|
||||
expect(response.body).to match "\"url\":\"/uploads/-/system/temp"
|
||||
it 'returns a content with original filename, new link, and correct type.' do
|
||||
subject
|
||||
|
||||
expect(response.body).to match '\"alt\":\"doc_sample.txt\"'
|
||||
expect(response.body).to match "\"url\":\"/uploads/-/system/user/#{user.id}/"
|
||||
end
|
||||
|
||||
it 'creates a corresponding Upload record' do
|
||||
expect { subject }.to change { Upload.count }
|
||||
|
||||
upload = Upload.last
|
||||
|
||||
aggregate_failures do
|
||||
expect(upload).to exist
|
||||
expect(upload.model).to eq user
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'does not create an Upload record' do
|
||||
expect { subject }.not_to change { Upload.count }
|
||||
it 'returns 404 when given user is not the logged in one' do
|
||||
another_user = create(:user)
|
||||
|
||||
post :create, params: { model: model, id: another_user.id, file: txt }, format: :json
|
||||
|
||||
expect(response).to have_gitlab_http_status(404)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -260,6 +260,7 @@ FactoryBot.define do
|
|||
trait(:merge_requests_enabled) { merge_requests_access_level ProjectFeature::ENABLED }
|
||||
trait(:merge_requests_disabled) { merge_requests_access_level ProjectFeature::DISABLED }
|
||||
trait(:merge_requests_private) { merge_requests_access_level ProjectFeature::PRIVATE }
|
||||
trait(:merge_requests_public) { merge_requests_access_level ProjectFeature::PUBLIC }
|
||||
trait(:repository_enabled) { repository_access_level ProjectFeature::ENABLED }
|
||||
trait(:repository_disabled) { repository_access_level ProjectFeature::DISABLED }
|
||||
trait(:repository_private) { repository_access_level ProjectFeature::PRIVATE }
|
||||
|
|
|
@ -41,7 +41,7 @@ describe 'User creates snippet', :js do
|
|||
expect(page).to have_content('My Snippet')
|
||||
|
||||
link = find('a.no-attachment-icon img[alt="banana_sample"]')['src']
|
||||
expect(link).to match(%r{/uploads/-/system/temp/\h{32}/banana_sample\.gif\z})
|
||||
expect(link).to match(%r{/uploads/-/system/user/#{user.id}/\h{32}/banana_sample\.gif\z})
|
||||
|
||||
reqs = inspect_requests { visit(link) }
|
||||
expect(reqs.first.status_code).to eq(200)
|
||||
|
|
|
@ -669,9 +669,7 @@ describe IssuesFinder do
|
|||
end
|
||||
|
||||
it 'filters by confidentiality' do
|
||||
expect(Issue).to receive(:where).with(a_string_matching('confidential'), anything)
|
||||
|
||||
subject
|
||||
expect(subject.to_sql).to match("issues.confidential")
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -688,9 +686,7 @@ describe IssuesFinder do
|
|||
end
|
||||
|
||||
it 'filters by confidentiality' do
|
||||
expect(Issue).to receive(:where).with(a_string_matching('confidential'), anything)
|
||||
|
||||
subject
|
||||
expect(subject.to_sql).to match("issues.confidential")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ describe MergeRequestsFinder do
|
|||
end
|
||||
|
||||
context 'filtering by group' do
|
||||
it 'includes all merge requests when user has access exceluding merge requests from projects the user does not have access to' do
|
||||
it 'includes all merge requests when user has access excluding merge requests from projects the user does not have access to' do
|
||||
private_project = allow_gitaly_n_plus_1 { create(:project, :private, group: group) }
|
||||
private_project.add_guest(user)
|
||||
create(:merge_request, :simple, author: user, source_project: private_project, target_project: private_project)
|
||||
|
|
6
spec/graphql/types/label_type_spec.rb
Normal file
6
spec/graphql/types/label_type_spec.rb
Normal file
|
@ -0,0 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
describe GitlabSchema.types['Label'] do
|
||||
it { is_expected.to require_graphql_authorizations(:read_label) }
|
||||
end
|
|
@ -2,4 +2,5 @@ require 'spec_helper'
|
|||
|
||||
describe GitlabSchema.types['Metadata'] do
|
||||
it { expect(described_class.graphql_name).to eq('Metadata') }
|
||||
it { is_expected.to require_graphql_authorizations(:read_instance_metadata) }
|
||||
end
|
||||
|
|
|
@ -24,9 +24,5 @@ describe GitlabSchema.types['Query'] do
|
|||
is_expected.to have_graphql_type(Types::MetadataType)
|
||||
is_expected.to have_graphql_resolver(Resolvers::MetadataResolver)
|
||||
end
|
||||
|
||||
it 'authorizes with read_instance_metadata' do
|
||||
is_expected.to require_graphql_authorizations(:read_instance_metadata)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,6 +5,40 @@ describe API::Helpers::RelatedResourcesHelpers do
|
|||
Class.new.include(described_class).new
|
||||
end
|
||||
|
||||
describe '#expose_path' do
|
||||
let(:path) { '/api/v4/awesome_endpoint' }
|
||||
|
||||
context 'empty relative URL root' do
|
||||
before do
|
||||
stub_config_setting(relative_url_root: '')
|
||||
end
|
||||
|
||||
it 'returns the existing path' do
|
||||
expect(helpers.expose_path(path)).to eq(path)
|
||||
end
|
||||
end
|
||||
|
||||
context 'slash relative URL root' do
|
||||
before do
|
||||
stub_config_setting(relative_url_root: '/')
|
||||
end
|
||||
|
||||
it 'returns the existing path' do
|
||||
expect(helpers.expose_path(path)).to eq(path)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with relative URL root' do
|
||||
before do
|
||||
stub_config_setting(relative_url_root: '/gitlab/root')
|
||||
end
|
||||
|
||||
it 'returns the existing path' do
|
||||
expect(helpers.expose_path(path)).to eq("/gitlab/root" + path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#expose_url' do
|
||||
let(:path) { '/api/v4/awesome_endpoint' }
|
||||
subject(:url) { helpers.expose_url(path) }
|
||||
|
|
|
@ -83,6 +83,11 @@ describe Banzai::Filter::RelativeLinkFilter do
|
|||
expect { filter(act) }.not_to raise_error
|
||||
end
|
||||
|
||||
it 'does not explode with an escaped null byte' do
|
||||
act = link("/%00")
|
||||
expect { filter(act) }.not_to raise_error
|
||||
end
|
||||
|
||||
it 'does not raise an exception with a space in the path' do
|
||||
act = link("/uploads/d18213acd3732630991986120e167e3d/Landscape_8.jpg \nBut here's some more unexpected text :smile:)")
|
||||
expect { filter(act) }.not_to raise_error
|
||||
|
|
|
@ -90,6 +90,27 @@ describe Gitlab::Ci::Config do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when yml is too big' do
|
||||
let(:yml) do
|
||||
<<~YAML
|
||||
--- &1
|
||||
- hi
|
||||
- *1
|
||||
YAML
|
||||
end
|
||||
|
||||
describe '.new' do
|
||||
it 'raises error' do
|
||||
expect(Gitlab::Sentry).to receive(:track_exception)
|
||||
|
||||
expect { config }.to raise_error(
|
||||
described_class::ConfigError,
|
||||
/The parsed YAML is too big/
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when config logic is incorrect' do
|
||||
let(:yml) { 'before_script: "ls"' }
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ describe Gitlab::Config::Loader::Yaml do
|
|||
|
||||
describe '#valid?' do
|
||||
it 'returns true' do
|
||||
expect(loader.valid?).to be true
|
||||
expect(loader).to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -24,7 +24,7 @@ describe Gitlab::Config::Loader::Yaml do
|
|||
|
||||
describe '#valid?' do
|
||||
it 'returns false' do
|
||||
expect(loader.valid?).to be false
|
||||
expect(loader).not_to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -43,7 +43,10 @@ describe Gitlab::Config::Loader::Yaml do
|
|||
|
||||
describe '#initialize' do
|
||||
it 'raises FormatError' do
|
||||
expect { loader }.to raise_error(Gitlab::Config::Loader::FormatError, 'Unknown alias: bad_alias')
|
||||
expect { loader }.to raise_error(
|
||||
Gitlab::Config::Loader::FormatError,
|
||||
'Unknown alias: bad_alias'
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -53,7 +56,68 @@ describe Gitlab::Config::Loader::Yaml do
|
|||
|
||||
describe '#valid?' do
|
||||
it 'returns false' do
|
||||
expect(loader.valid?).to be false
|
||||
expect(loader).not_to be_valid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Prevent Billion Laughs attack: https://gitlab.com/gitlab-org/gitlab-ce/issues/56018
|
||||
context 'when yaml size is too large' do
|
||||
let(:yml) do
|
||||
<<~YAML
|
||||
a: &a ["lol","lol","lol","lol","lol","lol","lol","lol","lol"]
|
||||
b: &b [*a,*a,*a,*a,*a,*a,*a,*a,*a]
|
||||
c: &c [*b,*b,*b,*b,*b,*b,*b,*b,*b]
|
||||
d: &d [*c,*c,*c,*c,*c,*c,*c,*c,*c]
|
||||
e: &e [*d,*d,*d,*d,*d,*d,*d,*d,*d]
|
||||
f: &f [*e,*e,*e,*e,*e,*e,*e,*e,*e]
|
||||
g: &g [*f,*f,*f,*f,*f,*f,*f,*f,*f]
|
||||
h: &h [*g,*g,*g,*g,*g,*g,*g,*g,*g]
|
||||
i: &i [*h,*h,*h,*h,*h,*h,*h,*h,*h]
|
||||
YAML
|
||||
end
|
||||
|
||||
describe '#valid?' do
|
||||
it 'returns false' do
|
||||
expect(loader).not_to be_valid
|
||||
end
|
||||
|
||||
it 'returns true if "ci_yaml_limit_size" feature flag is disabled' do
|
||||
stub_feature_flags(ci_yaml_limit_size: false)
|
||||
|
||||
expect(loader).to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
describe '#load!' do
|
||||
it 'raises FormatError' do
|
||||
expect { loader.load! }.to raise_error(
|
||||
Gitlab::Config::Loader::FormatError,
|
||||
'The parsed YAML is too big'
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Prevent Billion Laughs attack: https://gitlab.com/gitlab-org/gitlab-ce/issues/56018
|
||||
context 'when yaml has cyclic data structure' do
|
||||
let(:yml) do
|
||||
<<~YAML
|
||||
--- &1
|
||||
- hi
|
||||
- *1
|
||||
YAML
|
||||
end
|
||||
|
||||
describe '#valid?' do
|
||||
it 'returns false' do
|
||||
expect(loader.valid?).to be(false)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#load!' do
|
||||
it 'raises FormatError' do
|
||||
expect { loader.load! }.to raise_error(Gitlab::Config::Loader::FormatError, 'The parsed YAML is too big')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -50,5 +50,14 @@ describe Gitlab::DataBuilder::Pipeline do
|
|||
it { expect(attributes[:variables]).to be_a(Array) }
|
||||
it { expect(attributes[:variables]).to contain_exactly({ key: 'TRIGGER_KEY_1', value: 'TRIGGER_VALUE_1' }) }
|
||||
end
|
||||
|
||||
context 'when pipeline is a detached merge request pipeline' do
|
||||
let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) }
|
||||
let(:pipeline) { merge_request.all_pipelines.first }
|
||||
|
||||
it 'returns a source ref' do
|
||||
expect(attributes[:ref]).to eq(merge_request.source_branch)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -215,6 +215,18 @@ describe Gitlab::Git::Repository, :seed_helper do
|
|||
it { is_expected.to be < 2 }
|
||||
end
|
||||
|
||||
describe '#object_directory_size' do
|
||||
before do
|
||||
allow(repository.gitaly_repository_client)
|
||||
.to receive(:get_object_directory_size)
|
||||
.and_return(2)
|
||||
end
|
||||
|
||||
subject { repository.object_directory_size }
|
||||
|
||||
it { is_expected.to eq 2048 }
|
||||
end
|
||||
|
||||
describe '#empty?' do
|
||||
it { expect(repository).not_to be_empty }
|
||||
end
|
||||
|
|
|
@ -19,7 +19,9 @@ describe Gitlab::Git::Tree, :seed_helper do
|
|||
it 'returns a list of tree objects' do
|
||||
entries = described_class.where(repository, SeedRepo::Commit::ID, 'files', true)
|
||||
|
||||
expect(entries.count).to be >= 5
|
||||
expect(entries.map(&:path)).to include('files/html',
|
||||
'files/markdown/ruby-style-guide.md')
|
||||
expect(entries.count).to be >= 10
|
||||
expect(entries).to all(be_a(Gitlab::Git::Tree))
|
||||
end
|
||||
|
||||
|
|
|
@ -73,6 +73,17 @@ describe Gitlab::GitalyClient::RepositoryService do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#get_object_directory_size' do
|
||||
it 'sends a get_object_directory_size message' do
|
||||
expect_any_instance_of(Gitaly::RepositoryService::Stub)
|
||||
.to receive(:get_object_directory_size)
|
||||
.with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
|
||||
.and_return(size: 0)
|
||||
|
||||
client.get_object_directory_size
|
||||
end
|
||||
end
|
||||
|
||||
describe '#apply_gitattributes' do
|
||||
let(:revision) { 'master' }
|
||||
|
||||
|
|
|
@ -7,35 +7,39 @@ require 'spec_helper'
|
|||
describe Gitlab::Graphql::Authorize::AuthorizeFieldService do
|
||||
def type(type_authorizations = [])
|
||||
Class.new(Types::BaseObject) do
|
||||
graphql_name "TestType"
|
||||
graphql_name 'TestType'
|
||||
|
||||
authorize type_authorizations
|
||||
end
|
||||
end
|
||||
|
||||
def type_with_field(field_type, field_authorizations = [], resolved_value = "Resolved value")
|
||||
def type_with_field(field_type, field_authorizations = [], resolved_value = 'Resolved value', **options)
|
||||
Class.new(Types::BaseObject) do
|
||||
graphql_name "TestTypeWithField"
|
||||
field :test_field, field_type, null: true, authorize: field_authorizations, resolve: -> (_, _, _) { resolved_value}
|
||||
graphql_name 'TestTypeWithField'
|
||||
options.reverse_merge!(null: true)
|
||||
field :test_field, field_type,
|
||||
authorize: field_authorizations,
|
||||
resolve: -> (_, _, _) { resolved_value },
|
||||
**options
|
||||
end
|
||||
end
|
||||
|
||||
let(:current_user) { double(:current_user) }
|
||||
subject(:service) { described_class.new(field) }
|
||||
|
||||
describe "#authorized_resolve" do
|
||||
let(:presented_object) { double("presented object") }
|
||||
let(:presented_type) { double("parent type", object: presented_object) }
|
||||
describe '#authorized_resolve' do
|
||||
let(:presented_object) { double('presented object') }
|
||||
let(:presented_type) { double('parent type', object: presented_object) }
|
||||
subject(:resolved) { service.authorized_resolve.call(presented_type, {}, { current_user: current_user }) }
|
||||
|
||||
context "scalar types" do
|
||||
shared_examples "checking permissions on the presented object" do
|
||||
it "checks the abilities on the object being presented and returns the value" do
|
||||
context 'scalar types' do
|
||||
shared_examples 'checking permissions on the presented object' do
|
||||
it 'checks the abilities on the object being presented and returns the value' do
|
||||
expected_permissions.each do |permission|
|
||||
spy_ability_check_for(permission, presented_object, passed: true)
|
||||
end
|
||||
|
||||
expect(resolved).to eq("Resolved value")
|
||||
expect(resolved).to eq('Resolved value')
|
||||
end
|
||||
|
||||
it "returns nil if the value wasn't authorized" do
|
||||
|
@ -45,47 +49,57 @@ describe Gitlab::Graphql::Authorize::AuthorizeFieldService do
|
|||
end
|
||||
end
|
||||
|
||||
context "when the field is a scalar type" do
|
||||
let(:field) { type_with_field(GraphQL::STRING_TYPE, :read_field).fields["testField"].to_graphql }
|
||||
context 'when the field is a built-in scalar type' do
|
||||
let(:field) { type_with_field(GraphQL::STRING_TYPE, :read_field).fields['testField'].to_graphql }
|
||||
let(:expected_permissions) { [:read_field] }
|
||||
|
||||
it_behaves_like "checking permissions on the presented object"
|
||||
it_behaves_like 'checking permissions on the presented object'
|
||||
end
|
||||
|
||||
context "when the field is a list of scalar types" do
|
||||
let(:field) { type_with_field([GraphQL::STRING_TYPE], :read_field).fields["testField"].to_graphql }
|
||||
context 'when the field is a list of scalar types' do
|
||||
let(:field) { type_with_field([GraphQL::STRING_TYPE], :read_field).fields['testField'].to_graphql }
|
||||
let(:expected_permissions) { [:read_field] }
|
||||
|
||||
it_behaves_like "checking permissions on the presented object"
|
||||
it_behaves_like 'checking permissions on the presented object'
|
||||
end
|
||||
end
|
||||
|
||||
context "when the field is a specific type" do
|
||||
context 'when the field is a specific type' do
|
||||
let(:custom_type) { type(:read_type) }
|
||||
let(:object_in_field) { double("presented in field") }
|
||||
let(:field) { type_with_field(custom_type, :read_field, object_in_field).fields["testField"].to_graphql }
|
||||
let(:object_in_field) { double('presented in field') }
|
||||
let(:field) { type_with_field(custom_type, :read_field, object_in_field).fields['testField'].to_graphql }
|
||||
|
||||
it "checks both field & type permissions" do
|
||||
it 'checks both field & type permissions' do
|
||||
spy_ability_check_for(:read_field, object_in_field, passed: true)
|
||||
spy_ability_check_for(:read_type, object_in_field, passed: true)
|
||||
|
||||
expect(resolved).to eq(object_in_field)
|
||||
end
|
||||
|
||||
it "returns nil if viewing was not allowed" do
|
||||
it 'returns nil if viewing was not allowed' do
|
||||
spy_ability_check_for(:read_field, object_in_field, passed: false)
|
||||
spy_ability_check_for(:read_type, object_in_field, passed: true)
|
||||
|
||||
expect(resolved).to be_nil
|
||||
end
|
||||
|
||||
context "when the field is a list" do
|
||||
let(:object_1) { double("presented in field 1") }
|
||||
let(:object_2) { double("presented in field 2") }
|
||||
let(:presented_types) { [double(object: object_1), double(object: object_2)] }
|
||||
let(:field) { type_with_field([custom_type], :read_field, presented_types).fields["testField"].to_graphql }
|
||||
context 'when the field is not nullable' do
|
||||
let(:field) { type_with_field(custom_type, [], object_in_field, null: false).fields['testField'].to_graphql }
|
||||
|
||||
it "checks all permissions" do
|
||||
it 'returns nil when viewing is not allowed' do
|
||||
spy_ability_check_for(:read_type, object_in_field, passed: false)
|
||||
|
||||
expect(resolved).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the field is a list' do
|
||||
let(:object_1) { double('presented in field 1') }
|
||||
let(:object_2) { double('presented in field 2') }
|
||||
let(:presented_types) { [double(object: object_1), double(object: object_2)] }
|
||||
let(:field) { type_with_field([custom_type], :read_field, presented_types).fields['testField'].to_graphql }
|
||||
|
||||
it 'checks all permissions' do
|
||||
allow(Ability).to receive(:allowed?) { true }
|
||||
|
||||
spy_ability_check_for(:read_field, object_1, passed: true)
|
||||
|
@ -96,7 +110,7 @@ describe Gitlab::Graphql::Authorize::AuthorizeFieldService do
|
|||
expect(resolved).to eq(presented_types)
|
||||
end
|
||||
|
||||
it "filters out objects that the user cannot see" do
|
||||
it 'filters out objects that the user cannot see' do
|
||||
allow(Ability).to receive(:allowed?) { true }
|
||||
|
||||
spy_ability_check_for(:read_type, object_1, passed: false)
|
||||
|
|
|
@ -7,11 +7,11 @@ describe Gitlab::IssuableMetadata do
|
|||
subject { Class.new { include Gitlab::IssuableMetadata }.new }
|
||||
|
||||
it 'returns an empty Hash if an empty collection is provided' do
|
||||
expect(subject.issuable_meta_data(Issue.none, 'Issue')).to eq({})
|
||||
expect(subject.issuable_meta_data(Issue.none, 'Issue', user)).to eq({})
|
||||
end
|
||||
|
||||
it 'raises an error when given a collection with no limit' do
|
||||
expect { subject.issuable_meta_data(Issue.all, 'Issue') }.to raise_error(/must have a limit/)
|
||||
expect { subject.issuable_meta_data(Issue.all, 'Issue', user) }.to raise_error(/must have a limit/)
|
||||
end
|
||||
|
||||
context 'issues' do
|
||||
|
@ -23,7 +23,7 @@ describe Gitlab::IssuableMetadata do
|
|||
let!(:closing_issues) { create(:merge_requests_closing_issues, issue: issue, merge_request: merge_request) }
|
||||
|
||||
it 'aggregates stats on issues' do
|
||||
data = subject.issuable_meta_data(Issue.all.limit(10), 'Issue')
|
||||
data = subject.issuable_meta_data(Issue.all.limit(10), 'Issue', user)
|
||||
|
||||
expect(data.count).to eq(2)
|
||||
expect(data[issue.id].upvotes).to eq(1)
|
||||
|
@ -46,7 +46,7 @@ describe Gitlab::IssuableMetadata do
|
|||
let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") }
|
||||
|
||||
it 'aggregates stats on merge requests' do
|
||||
data = subject.issuable_meta_data(MergeRequest.all.limit(10), 'MergeRequest')
|
||||
data = subject.issuable_meta_data(MergeRequest.all.limit(10), 'MergeRequest', user)
|
||||
|
||||
expect(data.count).to eq(2)
|
||||
expect(data[merge_request.id].upvotes).to eq(1)
|
||||
|
|
43
spec/lib/gitlab/utils/deep_size_spec.rb
Normal file
43
spec/lib/gitlab/utils/deep_size_spec.rb
Normal file
|
@ -0,0 +1,43 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::Utils::DeepSize do
|
||||
let(:data) do
|
||||
{
|
||||
a: [1, 2, 3],
|
||||
b: {
|
||||
c: [4, 5],
|
||||
d: [
|
||||
{ e: [[6], [7]] }
|
||||
]
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
let(:max_size) { 1.kilobyte }
|
||||
let(:max_depth) { 10 }
|
||||
let(:deep_size) { described_class.new(data, max_size: max_size, max_depth: max_depth) }
|
||||
|
||||
describe '#evaluate' do
|
||||
context 'when data within size and depth limits' do
|
||||
it 'returns true' do
|
||||
expect(deep_size).to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
context 'when data not within size limit' do
|
||||
let(:max_size) { 200.bytes }
|
||||
|
||||
it 'returns false' do
|
||||
expect(deep_size).not_to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
context 'when data not within depth limit' do
|
||||
let(:max_depth) { 2 }
|
||||
|
||||
it 'returns false' do
|
||||
expect(deep_size).not_to be_valid
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -70,6 +70,16 @@ describe Member do
|
|||
expect(child_member).not_to be_valid
|
||||
end
|
||||
|
||||
# Membership in a subgroup confers certain access rights, such as being
|
||||
# able to merge or push code to protected branches.
|
||||
it "is valid with an equal level" do
|
||||
child_member.access_level = GroupMember::DEVELOPER
|
||||
|
||||
child_member.validate
|
||||
|
||||
expect(child_member).to be_valid
|
||||
end
|
||||
|
||||
it "is valid with a higher level" do
|
||||
child_member.access_level = GroupMember::MAINTAINER
|
||||
|
||||
|
|
|
@ -3188,61 +3188,105 @@ describe Project do
|
|||
end
|
||||
|
||||
describe '.with_feature_available_for_user' do
|
||||
let!(:user) { create(:user) }
|
||||
let!(:feature) { MergeRequest }
|
||||
let!(:project) { create(:project, :public, :merge_requests_enabled) }
|
||||
let(:user) { create(:user) }
|
||||
let(:feature) { MergeRequest }
|
||||
|
||||
subject { described_class.with_feature_available_for_user(feature, user) }
|
||||
|
||||
context 'when user has access to project' do
|
||||
subject { described_class.with_feature_available_for_user(feature, user) }
|
||||
shared_examples 'feature disabled' do
|
||||
let(:project) { create(:project, :public, :merge_requests_disabled) }
|
||||
|
||||
it 'does not return projects with the project feature disabled' do
|
||||
is_expected.not_to include(project)
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'feature public' do
|
||||
let(:project) { create(:project, :public, :merge_requests_public) }
|
||||
|
||||
it 'returns projects with the project feature public' do
|
||||
is_expected.to include(project)
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'feature enabled' do
|
||||
let(:project) { create(:project, :public, :merge_requests_enabled) }
|
||||
|
||||
it 'returns projects with the project feature enabled' do
|
||||
is_expected.to include(project)
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'feature access level is nil' do
|
||||
let(:project) { create(:project, :public) }
|
||||
|
||||
it 'returns projects with the project feature access level nil' do
|
||||
project.project_feature.update(merge_requests_access_level: nil)
|
||||
|
||||
is_expected.to include(project)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with user' do
|
||||
before do
|
||||
project.add_guest(user)
|
||||
end
|
||||
|
||||
context 'when public project' do
|
||||
context 'when feature is public' do
|
||||
it 'returns project' do
|
||||
is_expected.to include(project)
|
||||
end
|
||||
end
|
||||
it_behaves_like 'feature disabled'
|
||||
it_behaves_like 'feature public'
|
||||
it_behaves_like 'feature enabled'
|
||||
it_behaves_like 'feature access level is nil'
|
||||
|
||||
context 'when feature is private' do
|
||||
let!(:project) { create(:project, :public, :merge_requests_private) }
|
||||
context 'when feature is private' do
|
||||
let(:project) { create(:project, :public, :merge_requests_private) }
|
||||
|
||||
it 'returns project when user has access to the feature' do
|
||||
project.add_maintainer(user)
|
||||
|
||||
is_expected.to include(project)
|
||||
end
|
||||
|
||||
it 'does not return project when user does not have the minimum access level required' do
|
||||
context 'when user does not has access to the feature' do
|
||||
it 'does not return projects with the project feature private' do
|
||||
is_expected.not_to include(project)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when private project' do
|
||||
let!(:project) { create(:project) }
|
||||
context 'when user has access to the feature' do
|
||||
it 'returns projects with the project feature private' do
|
||||
project.add_reporter(user)
|
||||
|
||||
it 'returns project when user has access to the feature' do
|
||||
project.add_maintainer(user)
|
||||
|
||||
is_expected.to include(project)
|
||||
end
|
||||
|
||||
it 'does not return project when user does not have the minimum access level required' do
|
||||
is_expected.not_to include(project)
|
||||
is_expected.to include(project)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user does not have access to project' do
|
||||
let!(:project) { create(:project) }
|
||||
context 'user is an admin' do
|
||||
let(:user) { create(:user, :admin) }
|
||||
|
||||
it 'does not return project when user cant access project' do
|
||||
is_expected.not_to include(project)
|
||||
it_behaves_like 'feature disabled'
|
||||
it_behaves_like 'feature public'
|
||||
it_behaves_like 'feature enabled'
|
||||
it_behaves_like 'feature access level is nil'
|
||||
|
||||
context 'when feature is private' do
|
||||
let(:project) { create(:project, :public, :merge_requests_private) }
|
||||
|
||||
it 'returns projects with the project feature private' do
|
||||
is_expected.to include(project)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'without user' do
|
||||
let(:user) { nil }
|
||||
|
||||
it_behaves_like 'feature disabled'
|
||||
it_behaves_like 'feature public'
|
||||
it_behaves_like 'feature enabled'
|
||||
it_behaves_like 'feature access level is nil'
|
||||
|
||||
context 'when feature is private' do
|
||||
let(:project) { create(:project, :public, :merge_requests_private) }
|
||||
|
||||
it 'does not return projects with the project feature private' do
|
||||
is_expected.not_to include(project)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -136,24 +136,6 @@ describe Ci::BuildRunnerPresenter do
|
|||
is_expected.to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when pipeline is detached merge request pipeline' do
|
||||
let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) }
|
||||
let(:pipeline) { merge_request.all_pipelines.first }
|
||||
let(:build) { create(:ci_build, ref: pipeline.ref, pipeline: pipeline) }
|
||||
|
||||
it 'returns the default git depth for pipelines for merge requests' do
|
||||
is_expected.to eq(described_class::DEFAULT_GIT_DEPTH_MERGE_REQUEST)
|
||||
end
|
||||
|
||||
context 'when pipeline is legacy detached merge request pipeline' do
|
||||
let(:merge_request) { create(:merge_request, :with_legacy_detached_merge_request_pipeline) }
|
||||
|
||||
it 'behaves as branch pipeline' do
|
||||
is_expected.to eq(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#refspecs' do
|
||||
|
@ -191,7 +173,9 @@ describe Ci::BuildRunnerPresenter do
|
|||
|
||||
it 'returns the correct refspecs' do
|
||||
is_expected
|
||||
.to contain_exactly('+refs/merge-requests/1/head:refs/merge-requests/1/head')
|
||||
.to contain_exactly('+refs/heads/*:refs/remotes/origin/*',
|
||||
'+refs/tags/*:refs/tags/*',
|
||||
'+refs/merge-requests/1/head:refs/merge-requests/1/head')
|
||||
end
|
||||
|
||||
context 'when pipeline is legacy detached merge request pipeline' do
|
||||
|
|
|
@ -236,7 +236,7 @@ describe API::Members do
|
|||
params: { user_id: stranger.id, access_level: Member::REPORTER }
|
||||
|
||||
expect(response).to have_gitlab_http_status(400)
|
||||
expect(json_response['message']['access_level']).to eq(["should be higher than Developer inherited membership from group #{parent.name}"])
|
||||
expect(json_response['message']['access_level']).to eq(["should be greater than or equal to Developer inherited membership from group #{parent.name}"])
|
||||
end
|
||||
|
||||
it 'creates the member if group level is lower', :nested_groups do
|
||||
|
|
|
@ -830,6 +830,31 @@ describe API::MergeRequests do
|
|||
end
|
||||
end
|
||||
|
||||
context 'head_pipeline' do
|
||||
before do
|
||||
merge_request.update(head_pipeline: create(:ci_pipeline))
|
||||
merge_request.project.project_feature.update(builds_access_level: 10)
|
||||
end
|
||||
|
||||
context 'when user can read the pipeline' do
|
||||
it 'exposes pipeline information' do
|
||||
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user)
|
||||
|
||||
expect(json_response).to include('head_pipeline')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user can not read the pipeline' do
|
||||
let(:guest) { create(:user) }
|
||||
|
||||
it 'does not expose pipeline information' do
|
||||
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", guest)
|
||||
|
||||
expect(json_response).not_to include('head_pipeline')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns the commits behind the target branch when include_diverged_commits_count is present' do
|
||||
allow_any_instance_of(merge_request.class).to receive(:diverged_commits_count).and_return(1)
|
||||
|
||||
|
|
|
@ -504,8 +504,9 @@ describe API::Projects do
|
|||
project4.add_reporter(user2)
|
||||
end
|
||||
|
||||
it 'returns an array of groups the user has at least developer access' do
|
||||
it 'returns an array of projects the user has at least developer access' do
|
||||
get api('/projects', user2), params: { min_access_level: 30 }
|
||||
|
||||
expect(response).to have_gitlab_http_status(200)
|
||||
expect(response).to include_pagination_headers
|
||||
expect(json_response).to be_an Array
|
||||
|
|
|
@ -661,4 +661,24 @@ describe 'project routing' do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe Projects::TemplatesController, 'routing' do
|
||||
describe '#show' do
|
||||
def show_with_template_type(template_type)
|
||||
"/gitlab/gitlabhq/templates/#{template_type}/template_name"
|
||||
end
|
||||
|
||||
it 'routes when :template_type is `merge_request`' do
|
||||
expect(get(show_with_template_type('merge_request'))).to route_to('projects/templates#show', namespace_id: 'gitlab', project_id: 'gitlabhq', template_type: 'merge_request', key: 'template_name', format: 'json')
|
||||
end
|
||||
|
||||
it 'routes when :template_type is `issue`' do
|
||||
expect(get(show_with_template_type('issue'))).to route_to('projects/templates#show', namespace_id: 'gitlab', project_id: 'gitlabhq', template_type: 'issue', key: 'template_name', format: 'json')
|
||||
end
|
||||
|
||||
it 'routes to application#route_not_found when :template_type is unknown' do
|
||||
expect(get(show_with_template_type('invalid'))).to route_to('application#route_not_found', unmatched_route: 'gitlab/gitlabhq/templates/invalid/template_name')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
31
spec/routing/uploads_routing_spec.rb
Normal file
31
spec/routing/uploads_routing_spec.rb
Normal file
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe 'Uploads', 'routing' do
|
||||
it 'allows creating uploads for personal snippets' do
|
||||
expect(post('/uploads/personal_snippet?id=1')).to route_to(
|
||||
controller: 'uploads',
|
||||
action: 'create',
|
||||
model: 'personal_snippet',
|
||||
id: '1'
|
||||
)
|
||||
end
|
||||
|
||||
it 'allows creating uploads for users' do
|
||||
expect(post('/uploads/user?id=1')).to route_to(
|
||||
controller: 'uploads',
|
||||
action: 'create',
|
||||
model: 'user',
|
||||
id: '1'
|
||||
)
|
||||
end
|
||||
|
||||
it 'does not allow creating uploads for other models' do
|
||||
unroutable_models = UploadsController::MODEL_CLASSES.keys.compact - %w(personal_snippet user)
|
||||
|
||||
unroutable_models.each do |model|
|
||||
expect(post("/uploads/#{model}?id=1")).not_to be_routable
|
||||
end
|
||||
end
|
||||
end
|
|
@ -62,6 +62,25 @@ describe Lfs::FileTransformer do
|
|||
expect(result.encoding).to eq('text')
|
||||
end
|
||||
|
||||
context 'when an actual file is passed' do
|
||||
let(:file) { Tempfile.new(file_path) }
|
||||
|
||||
before do
|
||||
file.write(file_content)
|
||||
file.rewind
|
||||
end
|
||||
|
||||
after do
|
||||
file.unlink
|
||||
end
|
||||
|
||||
it "creates an LfsObject with the file's content" do
|
||||
subject.new_file(file_path, file)
|
||||
|
||||
expect(LfsObject.last.file.read).to eq file_content
|
||||
end
|
||||
end
|
||||
|
||||
context "when doesn't use LFS" do
|
||||
let(:file_path) { 'other.filetype' }
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ describe Projects::AfterImportService do
|
|||
describe '#execute' do
|
||||
before do
|
||||
allow(Projects::HousekeepingService)
|
||||
.to receive(:new).with(project, :gc).and_return(housekeeping_service)
|
||||
.to receive(:new).with(project).and_return(housekeeping_service)
|
||||
|
||||
allow(housekeeping_service)
|
||||
.to receive(:execute).and_yield
|
||||
|
|
|
@ -70,7 +70,7 @@ describe Projects::PropagateServiceTemplate do
|
|||
expect(project.pushover_service.properties).to eq(service_template.properties)
|
||||
end
|
||||
|
||||
describe 'bulk update' do
|
||||
describe 'bulk update', :use_sql_query_cache do
|
||||
let(:project_total) { 5 }
|
||||
|
||||
before do
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue