Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-10-03 09:09:43 +00:00
parent f743b42056
commit 10d4625ed3
35 changed files with 460 additions and 230 deletions

View File

@ -86,7 +86,7 @@ tasks:
printf "$(date) GitLab is up (took ~%.1f minutes)\n" "$((10*$SECONDS/60))e-1" | tee -a /workspace/startup.log printf "$(date) GitLab is up (took ~%.1f minutes)\n" "$((10*$SECONDS/60))e-1" | tee -a /workspace/startup.log
gp preview $(gp url 3000) || true gp preview $(gp url 3000) || true
PREBUILD_LOG=(/workspace/.gitpod/prebuild-log-*) PREBUILD_LOG=(/workspace/.gitpod/prebuild-log-*)
[[ -f /workspace/gitpod_start_time.sh ]] && printf "Took %.1f minutes from https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitpod.yml being executed through to completion %s\n" "$((10*(($(date +%s)-${START_TIME_IN_SECONDS}))/60))e-1" "$([[ -f "$PREBUILD_LOG" ]] && echo "With Prebuilds")" [[ -f /workspace/gitpod_start_time.sh ]] && printf "Took %.1f minutes from https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitpod.yml being executed through to completion %s\n" "$((10*(($(date +%s)-${START_TIME_IN_SECONDS}))/60))e-1" "$([[ -f "$PREBUILD_LOG" ]] && echo "With Prebuilds")"
) )
ports: ports:
@ -111,10 +111,10 @@ ports:
vscode: vscode:
extensions: extensions:
- rebornix.ruby@0.28.0 - rebornix.ruby@0.28.1
- wingrunr21.vscode-ruby@0.27.0 - wingrunr21.vscode-ruby@0.28.0
- karunamurti.haml@1.3.1 - karunamurti.haml@1.4.1
- octref.vetur@0.34.1 - octref.vetur@0.36.0
- dbaeumer.vscode-eslint@2.1.8 - dbaeumer.vscode-eslint@2.2.6
- gitlab.gitlab-workflow@3.24.0 - GitLab.gitlab-workflow@3.48.1
- DavidAnson.vscode-markdownlint@0.44.4 - DavidAnson.vscode-markdownlint@0.47.0

View File

@ -51,9 +51,12 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController
end end
def target def target
# type_id is not required in general
target_type = params.require(:type)
QuickActions::TargetService QuickActions::TargetService
.new(project, current_user) .new(project, current_user)
.execute(params[:type], params[:type_id]) .execute(target_type, params[:type_id])
end end
def authorize_read_crm_contact! def authorize_read_crm_contact!

View File

@ -49,6 +49,7 @@ module Resolvers
alert_management_alert: [:alert_management_alert], alert_management_alert: [:alert_management_alert],
labels: [:labels], labels: [:labels],
assignees: [:assignees], assignees: [:assignees],
participants: Issue.participant_includes,
timelogs: [:timelogs], timelogs: [:timelogs],
customer_relations_contacts: { customer_relations_contacts: [:group] }, customer_relations_contacts: { customer_relations_contacts: [:group] },
escalation_status: [:incident_management_issuable_escalation_status] escalation_status: [:incident_management_issuable_escalation_status]

View File

@ -73,4 +73,8 @@ class AwardEmoji < ApplicationRecord
awardable.expire_etag_cache if awardable.is_a?(Note) awardable.expire_etag_cache if awardable.is_a?(Note)
awardable.try(:update_upvotes_count) if upvote? awardable.try(:update_upvotes_count) if upvote?
end end
def to_ability_name
'emoji'
end
end end

View File

@ -231,7 +231,7 @@ module Issuable
class_methods do class_methods do
def participant_includes def participant_includes
[:assignees, :author, { notes: [:author, :award_emoji] }] [:author, :award_emoji, { notes: [:author, :award_emoji, :system_note_metadata] }]
end end
# Searches for records with a matching title. # Searches for records with a matching title.
@ -641,6 +641,14 @@ module Issuable
def draftless_title_changed(old_title) def draftless_title_changed(old_title)
old_title != title old_title != title
end end
def read_ability_for(participable_source)
return super if participable_source == self
name = participable_source.try(:issuable_ability_name) || :read_issuable_participables
{ name: name, subject: self }
end
end end
Issuable.prepend_mod_with('Issuable') Issuable.prepend_mod_with('Issuable')

View File

@ -152,7 +152,9 @@ module Participable
end end
def source_visible_to_user?(source, user) def source_visible_to_user?(source, user)
Ability.allowed?(user, "read_#{source.model_name.element}".to_sym, source) ability = read_ability_for(source)
Ability.allowed?(user, ability[:name], ability[:subject])
end end
def filter_by_ability(participants) def filter_by_ability(participants)
@ -172,6 +174,14 @@ module Participable
participant.can?(:read_project, project) participant.can?(:read_project, project)
end end
end end
# Returns Hash containing ability name and subject needed to read a specific participable.
# Should be overridden if a different ability is required.
def read_ability_for(participable_source)
name = participable_source.try(:to_ability_name) || participable_source.model_name.element
{ name: "read_#{name}".to_sym, subject: participable_source }
end
end end
Participable.prepend_mod_with('Participable') Participable.prepend_mod_with('Participable')

View File

@ -276,6 +276,10 @@ class Issue < ApplicationRecord
end end
end end
def self.participant_includes
[:assignees] + super
end
def next_object_by_relative_position(ignoring: nil, order: :asc) def next_object_by_relative_position(ignoring: nil, order: :asc)
array_mapping_scope = -> (id_expression) do array_mapping_scope = -> (id_expression) do
relation = Issue.where(Issue.arel_table[:project_id].eq(id_expression)) relation = Issue.where(Issue.arel_table[:project_id].eq(id_expression))

View File

@ -610,7 +610,7 @@ class MergeRequest < ApplicationRecord
end end
def self.participant_includes def self.participant_includes
[:reviewers, :award_emoji] + super [:assignees, :reviewers] + super
end end
def committers def committers

View File

@ -706,6 +706,10 @@ class Note < ApplicationRecord
super.sub!('task', 'checklist item') super.sub!('task', 'checklist item')
end end
def issuable_ability_name
confidential? ? :read_internal_note : :read_note
end
private private
def system_note_viewable_by?(user) def system_note_viewable_by?(user)

View File

@ -58,6 +58,12 @@ class IssuablePolicy < BasePolicy
rule { can_read_issuable }.policy do rule { can_read_issuable }.policy do
enable :read_issuable enable :read_issuable
enable :read_issuable_participables
end
# This rule replicates permissions in NotePolicy#can_read_confidential
rule { can?(:reporter_access) | assignee_or_author | admin }.policy do
enable :read_internal_note
end end
end end

View File

@ -20,7 +20,8 @@ class NotePolicy < BasePolicy
condition(:confidential, scope: :subject) { @subject.confidential? } condition(:confidential, scope: :subject) { @subject.confidential? }
# If this condition changes IssuablePolicy#read_confidential_notes should be updated too # Should be matched with IssuablePolicy#read_internal_note
# and EpicPolicy#read_internal_note
condition(:can_read_confidential) do condition(:can_read_confidential) do
access_level >= Gitlab::Access::REPORTER || @subject.noteable_assignee_or_author?(@user) || admin? access_level >= Gitlab::Access::REPORTER || @subject.noteable_assignee_or_author?(@user) || admin?
end end

View File

@ -32,6 +32,8 @@ module Users
end end
def groups def groups
return [] unless current_user
current_user.authorized_groups.with_route.sort_by(&:path) current_user.authorized_groups.with_route.sort_by(&:path)
end end

View File

@ -24,7 +24,7 @@ module Projects
end end
def commands(noteable, type) def commands(noteable, type)
return [] unless noteable return [] unless noteable && current_user
QuickActions::InterpretService.new(project, current_user).available_commands(noteable) QuickActions::InterpretService.new(project, current_user).available_commands(noteable)
end end

View File

@ -1,8 +0,0 @@
---
name: container_registry_tags_list_default_page_size
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/98215
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/374073
milestone: '15.5'
type: development
group: group::package
default_enabled: false

View File

@ -65,7 +65,7 @@ GET /issues?state=opened
| `created_before` | datetime | no | Return issues created on or before the given time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`) | | `created_before` | datetime | no | Return issues created on or before the given time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`) |
| `due_date` | string | no | Return issues that have no due date, are overdue, or whose due date is this week, this month, or between two weeks ago and next month. Accepts: `0` (no due date), `any`, `today`, `tomorrow`, `overdue`, `week`, `month`, `next_month_and_previous_two_weeks`. | | `due_date` | string | no | Return issues that have no due date, are overdue, or whose due date is this week, this month, or between two weeks ago and next month. Accepts: `0` (no due date), `any`, `today`, `tomorrow`, `overdue`, `week`, `month`, `next_month_and_previous_two_weeks`. |
| `epic_id` **(PREMIUM)** | integer | no | Return issues associated with the given epic ID. `None` returns issues that are not associated with an epic. `Any` returns issues that are associated with an epic. _([Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46887) in GitLab 13.6)_ | `epic_id` **(PREMIUM)** | integer | no | Return issues associated with the given epic ID. `None` returns issues that are not associated with an epic. `Any` returns issues that are associated with an epic. _([Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46887) in GitLab 13.6)_
| `health_status` **(ULTIMATE)** | string | no | Return issues with the specified `health_status`. _([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/370721) in GitLab 15.4)_ | `health_status` **(ULTIMATE)** | string | no | Return issues with the specified `health_status`. _([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/370721) in GitLab 15.4)._ In [GitLab 15.5 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/370721), `None` returns issues with no health status assigned, and `Any` returns issues with a health status assigned.
| `iids[]` | integer array | no | Return only the issues having the given `iid` | | `iids[]` | integer array | no | Return only the issues having the given `iid` |
| `in` | string | no | Modify the scope of the `search` attribute. `title`, `description`, or a string joining them with comma. Default is `title,description` | | `in` | string | no | Modify the scope of the `search` attribute. `title`, `description`, or a string joining them with comma. Default is `title,description` |
| `issue_type` | string | no | Filter to a given type of issue. One of `issue`, `incident`, or `test_case`. _([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/260375) in GitLab 13.12)_ | | `issue_type` | string | no | Filter to a given type of issue. One of `issue`, `incident`, or `test_case`. _([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/260375) in GitLab 13.12)_ |

View File

@ -9,8 +9,9 @@ info: To determine the technical writer assigned to the Stage/Group associated w
NOTE: NOTE:
We recommend you use the [Metadata API](metadata.md) instead of the Version API. We recommend you use the [Metadata API](metadata.md) instead of the Version API.
It contains additional information and is aligned with the GraphQL metadata endpoint. It contains additional information and is aligned with the GraphQL metadata endpoint.
As of GitLab 15.5, the Version API is a mirror of the Metadata API.
Retrieve version information for this GitLab instance. Responds `200 OK` for Retrieves version information for the GitLab instance. Responds with `200 OK` for
authenticated users. authenticated users.
```plaintext ```plaintext
@ -21,7 +22,13 @@ GET /version
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/version" curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/version"
``` ```
Example response: ## Example responses
### GitLab 15.5 and later
See [Metadata API](metadata.md) for the response.
### GitLab 15.4 and earlier
```json ```json
{ {

View File

@ -12,7 +12,8 @@ module API
%w[group project].each do |source_type| %w[group project].each do |source_type|
params do params do
requires :id, type: String, desc: "The #{source_type} ID" requires :id, type: String,
desc: "The ID or URL-encoded path of the #{source_type} owned by the authenticated user"
end end
resource source_type.pluralize, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do resource source_type.pluralize, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc "Gets a list of access requests for a #{source_type}." do desc "Gets a list of access requests for a #{source_type}." do
@ -54,7 +55,8 @@ module API
end end
params do params do
requires :user_id, type: Integer, desc: 'The user ID of the access requester' requires :user_id, type: Integer, desc: 'The user ID of the access requester'
optional :access_level, type: Integer, desc: 'A valid access level (defaults: `30`, developer access level)' optional :access_level, type: Integer, desc: 'A valid access level (defaults: `30`, the Developer role)',
default: 30
end end
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
put ':id/access_requests/:user_id/approve' do put ':id/access_requests/:user_id/approve' do

View File

@ -316,7 +316,6 @@ module API
mount ::API::UsageDataQueries mount ::API::UsageDataQueries
mount ::API::UserCounts mount ::API::UserCounts
mount ::API::Users mount ::API::Users
mount ::API::Version
mount ::API::Wikis mount ::API::Wikis
mount ::API::Ml::Mlflow mount ::API::Ml::Mlflow
end end

View File

@ -38,6 +38,25 @@ module API
present resource_group, with: Entities::Ci::ResourceGroup present resource_group, with: Entities::Ci::ResourceGroup
end end
desc 'List upcoming jobs of a resource group' do
success Entities::Ci::JobBasic
end
params do
requires :key, type: String, desc: 'The key of the resource group'
use :pagination
end
get ':id/resource_groups/:key/upcoming_jobs' do
authorize! :read_resource_group, resource_group
authorize! :read_build, user_project
upcoming_processables = resource_group
.upcoming_processables
.preload(:user, pipeline: :project) # rubocop:disable CodeReuse/ActiveRecord
present paginate(upcoming_processables), with: Entities::Ci::JobBasic
end
desc 'Edit a resource group' do desc 'Edit a resource group' do
success Entities::Ci::ResourceGroup success Entities::Ci::ResourceGroup
end end

View File

@ -25,15 +25,31 @@ module API
} }
EOF EOF
helpers do
def run_metadata_query
run_graphql!(
query: METADATA_QUERY,
context: { current_user: current_user },
transform: ->(result) { result.dig('data', 'metadata') }
)
end
end
desc 'Get the metadata information of the GitLab instance.' do desc 'Get the metadata information of the GitLab instance.' do
detail 'This feature was introduced in GitLab 15.2.' detail 'This feature was introduced in GitLab 15.2.'
end end
get '/metadata' do get '/metadata' do
run_graphql!( run_metadata_query
query: METADATA_QUERY, end
context: { current_user: current_user },
transform: ->(result) { result.dig('data', 'metadata') } # Support the deprecated `/version` route.
) # See https://gitlab.com/gitlab-org/gitlab/-/issues/366287
desc 'Get the version information of the GitLab instance.' do
detail 'This feature was introduced in GitLab 8.13 and deprecated in 15.5. ' \
'We recommend you instead use the Metadata API.'
end
get '/version' do
run_metadata_query
end end
end end
end end

View File

@ -1,34 +0,0 @@
# frozen_string_literal: true
module API
class Version < ::API::Base
helpers ::API::Helpers::GraphqlHelpers
include APIGuard
allow_access_with_scope :read_user, if: -> (request) { request.get? || request.head? }
before { authenticate! }
feature_category :not_owned # rubocop:todo Gitlab/AvoidFeatureCategoryNotOwned
METADATA_QUERY = <<~EOF
{
metadata {
version
revision
}
}
EOF
desc 'Get the version information of the GitLab instance.' do
detail 'This feature was introduced in GitLab 8.13.'
end
get '/version' do
run_graphql!(
query: METADATA_QUERY,
context: { current_user: current_user },
transform: ->(result) { result.dig('data', 'metadata') }
)
end
end
end

View File

@ -56,7 +56,7 @@ module ContainerRegistry
def repository_tags(name, page_size: DEFAULT_TAGS_PAGE_SIZE) def repository_tags(name, page_size: DEFAULT_TAGS_PAGE_SIZE)
response = faraday.get("/v2/#{name}/tags/list") do |req| response = faraday.get("/v2/#{name}/tags/list") do |req|
req.params['n'] = page_size if Feature.enabled?(:container_registry_tags_list_default_page_size) req.params['n'] = page_size
end end
response_body(response) response_body(response)
end end

View File

@ -5,37 +5,133 @@ require 'spec_helper'
RSpec.describe Projects::AutocompleteSourcesController do RSpec.describe Projects::AutocompleteSourcesController do
let_it_be(:group, reload: true) { create(:group) } let_it_be(:group, reload: true) { create(:group) }
let_it_be(:project) { create(:project, namespace: group) } let_it_be(:project) { create(:project, namespace: group) }
let_it_be(:issue) { create(:issue, project: project) } let_it_be(:public_project) { create(:project, :public, group: group) }
let_it_be(:development) { create(:label, project: project, name: 'Development') }
let_it_be(:issue) { create(:labeled_issue, project: project, labels: [development]) }
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
def members_by_username(username) def members_by_username(username)
json_response.find { |member| member['username'] == username } json_response.find { |member| member['username'] == username }
end end
describe 'GET members' do describe 'GET commands' do
before do
group.add_owner(user)
end
context 'with a public project' do
shared_examples 'issuable commands' do
it 'returns empty array when no user logged in' do
get :commands, format: :json, params: { namespace_id: group.path, project_id: public_project.path, type: issuable_type, type_id: issuable_iid }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to eq([])
end
it 'raises an error when no target type specified' do
sign_in(user)
expect { get :commands, format: :json, params: { namespace_id: group.path, project_id: project.path } }
.to raise_error(ActionController::ParameterMissing)
end
it 'returns an array of commands' do
sign_in(user)
get :commands, format: :json, params: { namespace_id: group.path, project_id: public_project.path, type: issuable_type, type_id: issuable_iid }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_present
end
end
context 'with an issue' do
let(:issuable_type) { issue.class.name }
let(:issuable_iid) { issue.iid }
it_behaves_like 'issuable commands'
end
context 'with merge request' do
let(:merge_request) { create(:merge_request, target_project: public_project, source_project: public_project) }
let(:issuable_type) { merge_request.class.name }
let(:issuable_iid) { merge_request.iid }
it_behaves_like 'issuable commands'
end
end
end
describe 'GET labels' do
before do before do
group.add_owner(user) group.add_owner(user)
sign_in(user) sign_in(user)
end end
it 'returns an array of member object' do it 'raises an error when no target type specified' do
get :members, format: :json, params: { namespace_id: group.path, project_id: project.path, type: issue.class.name, type_id: issue.id } expect { get :labels, format: :json, params: { namespace_id: group.path, project_id: project.path } }
.to raise_error(ActionController::ParameterMissing)
end
expect(members_by_username('all').symbolize_keys).to include( it 'returns an array of labels' do
username: 'all', get :labels, format: :json, params: { namespace_id: group.path, project_id: project.path, type: issue.class.name, type_id: issue.id }
name: 'All Project and Group Members',
count: 1)
expect(members_by_username(group.full_path).symbolize_keys).to include( expect(json_response).to be_a(Array)
type: group.class.name, expect(json_response.count).to eq(1)
name: group.full_name, expect(json_response[0]['title']).to eq('Development')
avatar_url: group.avatar_url, end
count: 1) end
expect(members_by_username(user.username).symbolize_keys).to include( describe 'GET members' do
type: user.class.name, context 'when logged in' do
name: user.name, before do
avatar_url: user.avatar_url) group.add_owner(user)
sign_in(user)
end
it 'returns 400 when no target type specified' do
expect { get :members, format: :json, params: { namespace_id: group.path, project_id: project.path } }
.to raise_error(ActionController::ParameterMissing)
end
it 'returns an array of member object' do
get :members, format: :json, params: { namespace_id: group.path, project_id: project.path, type: issue.class.name, type_id: issue.id }
expect(members_by_username('all').symbolize_keys).to include(
username: 'all',
name: 'All Project and Group Members',
count: 1)
expect(members_by_username(group.full_path).symbolize_keys).to include(
type: group.class.name,
name: group.full_name,
avatar_url: group.avatar_url,
count: 1)
expect(members_by_username(user.username).symbolize_keys).to include(
type: user.class.name,
name: user.name,
avatar_url: user.avatar_url)
end
end
context 'when anonymous' do
it 'redirects to login page' do
get :members, format: :json, params: { namespace_id: group.path, project_id: project.path, type: issue.class.name, type_id: issue.id }
expect(response).to redirect_to new_user_session_path
end
context 'with public project' do
it 'returns no members' do
get :members, format: :json, params: { namespace_id: group.path, project_id: public_project.path, type: issue.class.name, type_id: issue.id }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_a(Array)
expect(json_response.count).to eq(1)
expect(json_response.first['count']).to eq(0)
end
end
end end
end end

View File

@ -10,18 +10,31 @@ RSpec.describe Resolvers::Users::ParticipantsResolver do
let_it_be(:guest) { create(:user) } let_it_be(:guest) { create(:user) }
let_it_be(:project) { create(:project, :public) } let_it_be(:project) { create(:project, :public) }
let_it_be(:issue) { create(:issue, project: project) } let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:note) do
create(
:note,
:system,
:confidential,
project: project,
noteable: issue,
author: create(:user)
)
end
let_it_be(:note_metadata) { create(:system_note_metadata, note: note) } let_it_be(:public_note_author) { create(:user) }
let_it_be(:public_reply_author) { create(:user) }
let_it_be(:internal_note_author) { create(:user) }
let_it_be(:internal_reply_author) { create(:user) }
let_it_be(:public_note) { create(:note, project: project, noteable: issue, author: public_note_author) }
let_it_be(:internal_note) { create(:note, :confidential, project: project, noteable: issue, author: internal_note_author) }
let_it_be(:public_reply) { create(:note, noteable: issue, in_reply_to: public_note, project: project, author: public_reply_author) }
let_it_be(:internal_reply) { create(:note, :confidential, noteable: issue, in_reply_to: internal_note, project: project, author: internal_reply_author) }
let_it_be(:note_metadata2) { create(:system_note_metadata, note: public_note) }
let_it_be(:issue_emoji) { create(:award_emoji, name: 'thumbsup', awardable: issue) }
let_it_be(:note_emoji1) { create(:award_emoji, name: 'thumbsup', awardable: public_note) }
let_it_be(:note_emoji2) { create(:award_emoji, name: 'thumbsup', awardable: internal_note) }
let_it_be(:note_emoji3) { create(:award_emoji, name: 'thumbsup', awardable: public_reply) }
let_it_be(:note_emoji4) { create(:award_emoji, name: 'thumbsup', awardable: internal_reply) }
let_it_be(:issue_emoji_author) { issue_emoji.user }
let_it_be(:public_note_emoji_author) { note_emoji1.user }
let_it_be(:internal_note_emoji_author) { note_emoji2.user }
let_it_be(:public_reply_emoji_author) { note_emoji3.user }
let_it_be(:internal_reply_emoji_author) { note_emoji4.user }
subject(:resolved_items) { resolve(described_class, args: {}, ctx: { current_user: current_user }, obj: issue)&.items } subject(:resolved_items) { resolve(described_class, args: {}, ctx: { current_user: current_user }, obj: issue)&.items }
@ -34,7 +47,16 @@ RSpec.describe Resolvers::Users::ParticipantsResolver do
let(:current_user) { nil } let(:current_user) { nil }
it 'returns only publicly visible participants for this user' do it 'returns only publicly visible participants for this user' do
is_expected.to match_array([issue.author]) is_expected.to match_array(
[
issue.author,
issue_emoji_author,
public_note_author,
public_note_emoji_author,
public_reply_author,
public_reply_emoji_author
]
)
end end
end end
@ -42,15 +64,37 @@ RSpec.describe Resolvers::Users::ParticipantsResolver do
let(:current_user) { guest } let(:current_user) { guest }
it 'returns only publicly visible participants for this user' do it 'returns only publicly visible participants for this user' do
is_expected.to match_array([issue.author]) is_expected.to match_array(
[
issue.author,
issue_emoji_author,
public_note_author,
public_note_emoji_author,
public_reply_author,
public_reply_emoji_author
]
)
end end
end end
context 'when current user has access to confidential notes' do context 'when current user has access to internal notes' do
let(:current_user) { user } let(:current_user) { user }
it 'returns all participants for this user' do it 'returns all participants for this user' do
is_expected.to match_array([issue.author, note.author]) is_expected.to match_array(
[
issue.author,
issue_emoji_author,
public_note_author,
public_note_emoji_author,
public_reply_author,
internal_note_author,
internal_note_emoji_author,
internal_reply_author,
public_reply_emoji_author,
internal_reply_emoji_author
]
)
end end
context 'N+1 queries' do context 'N+1 queries' do
@ -64,9 +108,14 @@ RSpec.describe Resolvers::Users::ParticipantsResolver do
it 'does not execute N+1 for project relation' do it 'does not execute N+1 for project relation' do
control_count = ActiveRecord::QueryRecorder.new { query.call } control_count = ActiveRecord::QueryRecorder.new { query.call }
create(:note, :confidential, project: project, noteable: issue, author: create(:user)) create(:award_emoji, :upvote, awardable: issue)
internal_note = create(:note, :confidential, project: project, noteable: issue, author: create(:user))
create(:award_emoji, name: 'thumbsup', awardable: internal_note)
public_note = create(:note, project: project, noteable: issue, author: create(:user))
create(:award_emoji, name: 'thumbsup', awardable: public_note)
expect { query.call }.not_to exceed_query_limit(control_count) # 1 extra query per source (3 emojis + 2 notes) to fetch participables collection
expect { query.call }.not_to exceed_query_limit(control_count).with_threshold(5)
end end
it 'does not execute N+1 for system note metadata relation' do it 'does not execute N+1 for system note metadata relation' do

View File

@ -437,18 +437,6 @@ RSpec.describe ContainerRegistry::Client do
expect(subject).to eq('tags' => %w[t1 t2]) expect(subject).to eq('tags' => %w[t1 t2])
end end
context 'with container_registry_tags_list_default_page_size disabled' do
before do
stub_feature_flags(container_registry_tags_list_default_page_size: false)
end
it 'returns a successful response' do
stub_registry_tags_list(tags: %w[t1 t2])
expect(subject).to eq('tags' => %w[t1 t2])
end
end
end end
describe '.registry_info' do describe '.registry_info' do

View File

@ -290,4 +290,13 @@ RSpec.describe AwardEmoji do
end end
end end
end end
describe '#to_ability_name' do
let(:merge_request) { create(:merge_request) }
let(:award_emoji) { build(:award_emoji, user: build(:user), awardable: merge_request) }
it 'returns correct ability name' do
expect(award_emoji.to_ability_name).to be('emoji')
end
end
end end

View File

@ -186,6 +186,9 @@ RSpec.describe Participable do
expect(instance.visible_participants(user1)).to match_array [user1, user2] expect(instance.visible_participants(user1)).to match_array [user1, user2]
end end
end end
it_behaves_like 'visible participants for issuable with read ability', :issue
it_behaves_like 'visible participants for issuable with read ability', :merge_request
end end
describe '#participant?' do describe '#participant?' do

View File

@ -31,8 +31,8 @@ RSpec.describe IssuablePolicy, models: true do
expect(policies).to be_allowed(:resolve_note) expect(policies).to be_allowed(:resolve_note)
end end
it 'allows reading confidential notes' do it 'allows reading internal notes' do
expect(policies).to be_allowed(:read_confidential_notes) expect(policies).to be_allowed(:read_internal_note)
end end
context 'when user is able to read project' do context 'when user is able to read project' do
@ -94,8 +94,8 @@ RSpec.describe IssuablePolicy, models: true do
let(:issue) { create(:issue, project: project, assignees: [user]) } let(:issue) { create(:issue, project: project, assignees: [user]) }
let(:policies) { described_class.new(user, issue) } let(:policies) { described_class.new(user, issue) }
it 'allows reading confidential notes' do it 'allows reading internal notes' do
expect(policies).to be_allowed(:read_confidential_notes) expect(policies).to be_allowed(:read_internal_note)
end end
end end
@ -145,6 +145,10 @@ RSpec.describe IssuablePolicy, models: true do
it 'does not allow timelogs creation' do it 'does not allow timelogs creation' do
expect(policies).to be_disallowed(:create_timelog) expect(policies).to be_disallowed(:create_timelog)
end end
it 'does not allow reading internal notes' do
expect(permissions(guest, issue)).to be_disallowed(:read_internal_note)
end
end end
context 'when user is a guest member of the project' do context 'when user is a guest member of the project' do
@ -170,8 +174,8 @@ RSpec.describe IssuablePolicy, models: true do
expect(permissions(reporter, issue)).to be_allowed(:create_timelog) expect(permissions(reporter, issue)).to be_allowed(:create_timelog)
end end
it 'allows reading confidential notes' do it 'allows reading internal notes' do
expect(permissions(reporter, issue)).to be_allowed(:read_confidential_notes) expect(permissions(reporter, issue)).to be_allowed(:read_internal_note)
end end
end end
@ -188,6 +192,7 @@ RSpec.describe IssuablePolicy, models: true do
it 'does not allow :read_issuable' do it 'does not allow :read_issuable' do
expect(policy).not_to be_allowed(:read_issuable) expect(policy).not_to be_allowed(:read_issuable)
expect(policy).not_to be_allowed(:read_issuable_participables)
end end
end end
@ -196,6 +201,7 @@ RSpec.describe IssuablePolicy, models: true do
it 'allows :read_issuable' do it 'allows :read_issuable' do
expect(policy).to be_allowed(:read_issuable) expect(policy).to be_allowed(:read_issuable)
expect(policy).to be_allowed(:read_issuable_participables)
end end
end end
end end
@ -213,6 +219,7 @@ RSpec.describe IssuablePolicy, models: true do
it 'does not allow :read_issuable' do it 'does not allow :read_issuable' do
expect(policy).not_to be_allowed(:read_issuable) expect(policy).not_to be_allowed(:read_issuable)
expect(policy).not_to be_allowed(:read_issuable_participables)
end end
end end
@ -221,6 +228,7 @@ RSpec.describe IssuablePolicy, models: true do
it 'allows :read_issuable' do it 'allows :read_issuable' do
expect(policy).to be_allowed(:read_issuable) expect(policy).to be_allowed(:read_issuable)
expect(policy).to be_allowed(:read_issuable_participables)
end end
end end
end end

View File

@ -77,6 +77,48 @@ RSpec.describe API::Ci::ResourceGroups do
end end
end end
describe 'GET /projects/:id/resource_groups/:key/upcoming_jobs' do
subject { get api("/projects/#{project.id}/resource_groups/#{key}/upcoming_jobs", user) }
let_it_be(:resource_group) { create(:ci_resource_group, project: project) }
let_it_be(:processable) { create(:ci_processable, resource_group: resource_group) }
let_it_be(:upcoming_processable) { create(:ci_processable, :waiting_for_resource, resource_group: resource_group) }
let(:key) { resource_group.key }
it 'returns upcoming jobs of resource group', :aggregate_failures do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.length).to eq(1)
expect(json_response[0]['id']).to eq(upcoming_processable.id)
expect(json_response[0]['name']).to eq(upcoming_processable.name)
expect(json_response[0]['ref']).to eq(upcoming_processable.ref)
expect(json_response[0]['stage']).to eq(upcoming_processable.stage)
expect(json_response[0]['status']).to eq(upcoming_processable.status)
end
context 'when user is reporter' do
let(:user) { reporter }
it 'returns forbidden' do
subject
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when there is no corresponding resource group' do
let(:key) { 'unknown' }
it 'returns not found' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
describe 'PUT /projects/:id/resource_groups/:key' do describe 'PUT /projects/:id/resource_groups/:key' do
subject { put api("/projects/#{project.id}/resource_groups/#{key}", user), params: params } subject { put api("/projects/#{project.id}/resource_groups/#{key}", user), params: params }

View File

@ -686,6 +686,30 @@ RSpec.describe 'getting an issue list for a project' do
include_examples 'N+1 query check' include_examples 'N+1 query check'
end end
context 'when requesting participants' do
let_it_be(:issue_c) { create(:issue, project: project) }
let(:search_params) { { iids: [issue_a.iid.to_s, issue_c.iid.to_s] } }
let(:requested_fields) { 'participants { nodes { name } }' }
before do
create(:award_emoji, :upvote, awardable: issue_a)
create(:award_emoji, :upvote, awardable: issue_b)
create(:award_emoji, :upvote, awardable: issue_c)
note_with_emoji_a = create(:note_on_issue, noteable: issue_a, project: project)
note_with_emoji_b = create(:note_on_issue, noteable: issue_b, project: project)
note_with_emoji_c = create(:note_on_issue, noteable: issue_c, project: project)
create(:award_emoji, :upvote, awardable: note_with_emoji_a)
create(:award_emoji, :upvote, awardable: note_with_emoji_b)
create(:award_emoji, :upvote, awardable: note_with_emoji_c)
end
# Executes 3 extra queries to fetch participant_attrs
include_examples 'N+1 query check', 3
end
end end
def issues_ids def issues_ids

View File

@ -338,6 +338,27 @@ RSpec.describe 'getting merge request listings nested in a project' do
include_examples 'N+1 query check' include_examples 'N+1 query check'
end end
context 'when requesting participants' do
let(:requested_fields) { 'participants { nodes { name } }' }
before do
create(:award_emoji, :upvote, awardable: merge_request_a)
create(:award_emoji, :upvote, awardable: merge_request_b)
create(:award_emoji, :upvote, awardable: merge_request_c)
note_with_emoji_a = create(:note_on_merge_request, noteable: merge_request_a, project: project)
note_with_emoji_b = create(:note_on_merge_request, noteable: merge_request_b, project: project)
note_with_emoji_c = create(:note_on_merge_request, noteable: merge_request_c, project: project)
create(:award_emoji, :upvote, awardable: note_with_emoji_a)
create(:award_emoji, :upvote, awardable: note_with_emoji_b)
create(:award_emoji, :upvote, awardable: note_with_emoji_c)
end
# Executes 3 extra queries to fetch participant_attrs
include_examples 'N+1 query check', 3
end
end end
describe 'performance' do describe 'performance' do

View File

@ -6,7 +6,7 @@ RSpec.describe API::Metadata do
shared_examples_for 'GET /metadata' do shared_examples_for 'GET /metadata' do
context 'when unauthenticated' do context 'when unauthenticated' do
it 'returns authentication error' do it 'returns authentication error' do
get api('/metadata') get api(endpoint)
expect(response).to have_gitlab_http_status(:unauthorized) expect(response).to have_gitlab_http_status(:unauthorized)
end end
@ -16,7 +16,7 @@ RSpec.describe API::Metadata do
let(:user) { create(:user) } let(:user) { create(:user) }
it 'returns the metadata information' do it 'returns the metadata information' do
get api('/metadata', user) get api(endpoint, user)
expect_metadata expect_metadata
end end
@ -29,13 +29,13 @@ RSpec.describe API::Metadata do
let(:scopes) { %i(api) } let(:scopes) { %i(api) }
it 'returns the metadata information' do it 'returns the metadata information' do
get api('/metadata', personal_access_token: personal_access_token) get api(endpoint, personal_access_token: personal_access_token)
expect_metadata expect_metadata
end end
it 'returns "200" response on head requests' do it 'returns "200" response on head requests' do
head api('/metadata', personal_access_token: personal_access_token) head api(endpoint, personal_access_token: personal_access_token)
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
end end
@ -45,13 +45,13 @@ RSpec.describe API::Metadata do
let(:scopes) { %i(read_user) } let(:scopes) { %i(read_user) }
it 'returns the metadata information' do it 'returns the metadata information' do
get api('/metadata', personal_access_token: personal_access_token) get api(endpoint, personal_access_token: personal_access_token)
expect_metadata expect_metadata
end end
it 'returns "200" response on head requests' do it 'returns "200" response on head requests' do
head api('/metadata', personal_access_token: personal_access_token) head api(endpoint, personal_access_token: personal_access_token)
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
end end
@ -61,7 +61,7 @@ RSpec.describe API::Metadata do
let(:scopes) { %i(read_repository) } let(:scopes) { %i(read_repository) }
it 'returns authorization error' do it 'returns authorization error' do
get api('/metadata', personal_access_token: personal_access_token) get api(endpoint, personal_access_token: personal_access_token)
expect(response).to have_gitlab_http_status(:forbidden) expect(response).to have_gitlab_http_status(:forbidden)
end end
@ -76,18 +76,14 @@ RSpec.describe API::Metadata do
end end
end end
context 'with graphql enabled' do describe 'GET /metadata' do
before do let(:endpoint) { '/metadata' }
stub_feature_flags(graphql: true)
end
include_examples 'GET /metadata' include_examples 'GET /metadata'
end end
context 'with graphql disabled' do describe 'GET /version' do
before do let(:endpoint) { '/version' }
stub_feature_flags(graphql: false)
end
include_examples 'GET /metadata' include_examples 'GET /metadata'
end end

View File

@ -1,93 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::Version do
shared_examples_for 'GET /version' do
context 'when unauthenticated' do
it 'returns authentication error' do
get api('/version')
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
context 'when authenticated as user' do
let(:user) { create(:user) }
it 'returns the version information' do
get api('/version', user)
expect_version
end
end
context 'when authenticated with token' do
let(:personal_access_token) { create(:personal_access_token, scopes: scopes) }
context 'with api scope' do
let(:scopes) { %i(api) }
it 'returns the version information' do
get api('/version', personal_access_token: personal_access_token)
expect_version
end
it 'returns "200" response on head requests' do
head api('/version', personal_access_token: personal_access_token)
expect(response).to have_gitlab_http_status(:ok)
end
end
context 'with read_user scope' do
let(:scopes) { %i(read_user) }
it 'returns the version information' do
get api('/version', personal_access_token: personal_access_token)
expect_version
end
it 'returns "200" response on head requests' do
head api('/version', personal_access_token: personal_access_token)
expect(response).to have_gitlab_http_status(:ok)
end
end
context 'with neither api nor read_user scope' do
let(:scopes) { %i(read_repository) }
it 'returns authorization error' do
get api('/version', personal_access_token: personal_access_token)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
def expect_version
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['version']).to eq(Gitlab::VERSION)
expect(json_response['revision']).to eq(Gitlab.revision)
end
end
context 'with graphql enabled' do
before do
stub_feature_flags(graphql: true)
end
include_examples 'GET /version'
end
context 'with graphql disabled' do
before do
stub_feature_flags(graphql: false)
end
include_examples 'GET /version'
end
end

View File

@ -1,5 +1,5 @@
# frozen_string_literal: true # frozen_string_literal: true
RSpec.shared_examples 'N+1 query check' do RSpec.shared_examples 'N+1 query check' do |threshold = 0|
it 'prevents N+1 queries' do it 'prevents N+1 queries' do
execute_query # "warm up" to prevent undeterministic counts execute_query # "warm up" to prevent undeterministic counts
expect(graphql_errors).to be_blank # Sanity check - ex falso quodlibet! expect(graphql_errors).to be_blank # Sanity check - ex falso quodlibet!
@ -8,6 +8,7 @@ RSpec.shared_examples 'N+1 query check' do
expect(control.count).to be > 0 expect(control.count).to be > 0
search_params[:iids] << extra_iid_for_second_query search_params[:iids] << extra_iid_for_second_query
expect { execute_query }.not_to exceed_query_limit(control)
expect { execute_query }.not_to exceed_query_limit(control).with_threshold(threshold)
end end
end end

View File

@ -0,0 +1,42 @@
# frozen_string_literal: true
RSpec.shared_examples 'visible participants for issuable with read ability' do |model_class|
let_it_be(:user1) { create(:user) }
let(:model) { model_class.to_s.classify.constantize }
before do
allow(Ability).to receive(:allowed?).with(anything, :"read_#{model_class}", anything).and_return(true)
allow(model).to receive(:participant_attrs).and_return([:bar])
end
shared_examples 'check for participables read ability' do |ability_name|
it 'receives expected ability' do
instance = model.new
allow(instance).to receive(:bar).and_return(participable_source)
expect(Ability).to receive(:allowed?).with(anything, ability_name, instance)
expect(instance.visible_participants(user1)).to be_empty
end
end
context 'when source is an award emoji' do
let(:participable_source) { build(:award_emoji, :upvote) }
it_behaves_like 'check for participables read ability', :read_issuable_participables
end
context 'when source is a note' do
let(:participable_source) { build(:note) }
it_behaves_like 'check for participables read ability', :read_note
end
context 'when source is an internal note' do
let(:participable_source) { build(:note, :confidential) }
it_behaves_like 'check for participables read ability', :read_internal_note
end
end