Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
f743b42056
commit
10d4625ed3
16
.gitpod.yml
16
.gitpod.yml
|
@ -86,7 +86,7 @@ tasks:
|
|||
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
|
||||
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:
|
||||
|
@ -111,10 +111,10 @@ ports:
|
|||
|
||||
vscode:
|
||||
extensions:
|
||||
- rebornix.ruby@0.28.0
|
||||
- wingrunr21.vscode-ruby@0.27.0
|
||||
- karunamurti.haml@1.3.1
|
||||
- octref.vetur@0.34.1
|
||||
- dbaeumer.vscode-eslint@2.1.8
|
||||
- gitlab.gitlab-workflow@3.24.0
|
||||
- DavidAnson.vscode-markdownlint@0.44.4
|
||||
- rebornix.ruby@0.28.1
|
||||
- wingrunr21.vscode-ruby@0.28.0
|
||||
- karunamurti.haml@1.4.1
|
||||
- octref.vetur@0.36.0
|
||||
- dbaeumer.vscode-eslint@2.2.6
|
||||
- GitLab.gitlab-workflow@3.48.1
|
||||
- DavidAnson.vscode-markdownlint@0.47.0
|
||||
|
|
|
@ -51,9 +51,12 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def target
|
||||
# type_id is not required in general
|
||||
target_type = params.require(:type)
|
||||
|
||||
QuickActions::TargetService
|
||||
.new(project, current_user)
|
||||
.execute(params[:type], params[:type_id])
|
||||
.execute(target_type, params[:type_id])
|
||||
end
|
||||
|
||||
def authorize_read_crm_contact!
|
||||
|
|
|
@ -49,6 +49,7 @@ module Resolvers
|
|||
alert_management_alert: [:alert_management_alert],
|
||||
labels: [:labels],
|
||||
assignees: [:assignees],
|
||||
participants: Issue.participant_includes,
|
||||
timelogs: [:timelogs],
|
||||
customer_relations_contacts: { customer_relations_contacts: [:group] },
|
||||
escalation_status: [:incident_management_issuable_escalation_status]
|
||||
|
|
|
@ -73,4 +73,8 @@ class AwardEmoji < ApplicationRecord
|
|||
awardable.expire_etag_cache if awardable.is_a?(Note)
|
||||
awardable.try(:update_upvotes_count) if upvote?
|
||||
end
|
||||
|
||||
def to_ability_name
|
||||
'emoji'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -231,7 +231,7 @@ module Issuable
|
|||
|
||||
class_methods do
|
||||
def participant_includes
|
||||
[:assignees, :author, { notes: [:author, :award_emoji] }]
|
||||
[:author, :award_emoji, { notes: [:author, :award_emoji, :system_note_metadata] }]
|
||||
end
|
||||
|
||||
# Searches for records with a matching title.
|
||||
|
@ -641,6 +641,14 @@ module Issuable
|
|||
def draftless_title_changed(old_title)
|
||||
old_title != title
|
||||
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
|
||||
|
||||
Issuable.prepend_mod_with('Issuable')
|
||||
|
|
|
@ -152,7 +152,9 @@ module Participable
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
def filter_by_ability(participants)
|
||||
|
@ -172,6 +174,14 @@ module Participable
|
|||
participant.can?(:read_project, project)
|
||||
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
|
||||
|
||||
Participable.prepend_mod_with('Participable')
|
||||
|
|
|
@ -276,6 +276,10 @@ class Issue < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def self.participant_includes
|
||||
[:assignees] + super
|
||||
end
|
||||
|
||||
def next_object_by_relative_position(ignoring: nil, order: :asc)
|
||||
array_mapping_scope = -> (id_expression) do
|
||||
relation = Issue.where(Issue.arel_table[:project_id].eq(id_expression))
|
||||
|
|
|
@ -610,7 +610,7 @@ class MergeRequest < ApplicationRecord
|
|||
end
|
||||
|
||||
def self.participant_includes
|
||||
[:reviewers, :award_emoji] + super
|
||||
[:assignees, :reviewers] + super
|
||||
end
|
||||
|
||||
def committers
|
||||
|
|
|
@ -706,6 +706,10 @@ class Note < ApplicationRecord
|
|||
super.sub!('task', 'checklist item')
|
||||
end
|
||||
|
||||
def issuable_ability_name
|
||||
confidential? ? :read_internal_note : :read_note
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def system_note_viewable_by?(user)
|
||||
|
|
|
@ -58,6 +58,12 @@ class IssuablePolicy < BasePolicy
|
|||
|
||||
rule { can_read_issuable }.policy do
|
||||
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
|
||||
|
||||
|
|
|
@ -20,7 +20,8 @@ class NotePolicy < BasePolicy
|
|||
|
||||
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
|
||||
access_level >= Gitlab::Access::REPORTER || @subject.noteable_assignee_or_author?(@user) || admin?
|
||||
end
|
||||
|
|
|
@ -32,6 +32,8 @@ module Users
|
|||
end
|
||||
|
||||
def groups
|
||||
return [] unless current_user
|
||||
|
||||
current_user.authorized_groups.with_route.sort_by(&:path)
|
||||
end
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ module Projects
|
|||
end
|
||||
|
||||
def commands(noteable, type)
|
||||
return [] unless noteable
|
||||
return [] unless noteable && current_user
|
||||
|
||||
QuickActions::InterpretService.new(project, current_user).available_commands(noteable)
|
||||
end
|
||||
|
|
|
@ -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
|
|
@ -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`) |
|
||||
| `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)_
|
||||
| `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` |
|
||||
| `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)_ |
|
||||
|
|
|
@ -9,8 +9,9 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
NOTE:
|
||||
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.
|
||||
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.
|
||||
|
||||
```plaintext
|
||||
|
@ -21,7 +22,13 @@ GET /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
|
||||
{
|
||||
|
|
|
@ -12,7 +12,8 @@ module API
|
|||
|
||||
%w[group project].each do |source_type|
|
||||
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
|
||||
resource source_type.pluralize, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
|
||||
desc "Gets a list of access requests for a #{source_type}." do
|
||||
|
@ -54,7 +55,8 @@ module API
|
|||
end
|
||||
params do
|
||||
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
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
put ':id/access_requests/:user_id/approve' do
|
||||
|
|
|
@ -316,7 +316,6 @@ module API
|
|||
mount ::API::UsageDataQueries
|
||||
mount ::API::UserCounts
|
||||
mount ::API::Users
|
||||
mount ::API::Version
|
||||
mount ::API::Wikis
|
||||
mount ::API::Ml::Mlflow
|
||||
end
|
||||
|
|
|
@ -38,6 +38,25 @@ module API
|
|||
present resource_group, with: Entities::Ci::ResourceGroup
|
||||
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
|
||||
success Entities::Ci::ResourceGroup
|
||||
end
|
||||
|
|
|
@ -25,15 +25,31 @@ module API
|
|||
}
|
||||
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
|
||||
detail 'This feature was introduced in GitLab 15.2.'
|
||||
end
|
||||
get '/metadata' do
|
||||
run_graphql!(
|
||||
query: METADATA_QUERY,
|
||||
context: { current_user: current_user },
|
||||
transform: ->(result) { result.dig('data', 'metadata') }
|
||||
)
|
||||
run_metadata_query
|
||||
end
|
||||
|
||||
# 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
|
||||
|
|
|
@ -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
|
|
@ -56,7 +56,7 @@ module ContainerRegistry
|
|||
|
||||
def repository_tags(name, page_size: DEFAULT_TAGS_PAGE_SIZE)
|
||||
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
|
||||
response_body(response)
|
||||
end
|
||||
|
|
|
@ -5,37 +5,133 @@ require 'spec_helper'
|
|||
RSpec.describe Projects::AutocompleteSourcesController do
|
||||
let_it_be(:group, reload: true) { create(: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) }
|
||||
|
||||
def members_by_username(username)
|
||||
json_response.find { |member| member['username'] == username }
|
||||
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
|
||||
group.add_owner(user)
|
||||
sign_in(user)
|
||||
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 }
|
||||
it 'raises an error when no target type specified' do
|
||||
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(
|
||||
username: 'all',
|
||||
name: 'All Project and Group Members',
|
||||
count: 1)
|
||||
it 'returns an array of labels' do
|
||||
get :labels, format: :json, params: { namespace_id: group.path, project_id: project.path, type: issue.class.name, type_id: issue.id }
|
||||
|
||||
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(json_response).to be_a(Array)
|
||||
expect(json_response.count).to eq(1)
|
||||
expect(json_response[0]['title']).to eq('Development')
|
||||
end
|
||||
end
|
||||
|
||||
expect(members_by_username(user.username).symbolize_keys).to include(
|
||||
type: user.class.name,
|
||||
name: user.name,
|
||||
avatar_url: user.avatar_url)
|
||||
describe 'GET members' do
|
||||
context 'when logged in' do
|
||||
before do
|
||||
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
|
||||
|
||||
|
|
|
@ -10,18 +10,31 @@ RSpec.describe Resolvers::Users::ParticipantsResolver do
|
|||
let_it_be(:guest) { create(:user) }
|
||||
let_it_be(:project) { create(:project, :public) }
|
||||
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 }
|
||||
|
||||
|
@ -34,7 +47,16 @@ RSpec.describe Resolvers::Users::ParticipantsResolver do
|
|||
let(:current_user) { nil }
|
||||
|
||||
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
|
||||
|
||||
|
@ -42,15 +64,37 @@ RSpec.describe Resolvers::Users::ParticipantsResolver do
|
|||
let(:current_user) { guest }
|
||||
|
||||
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
|
||||
|
||||
context 'when current user has access to confidential notes' do
|
||||
context 'when current user has access to internal notes' do
|
||||
let(:current_user) { user }
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
it 'does not execute N+1 for system note metadata relation' do
|
||||
|
|
|
@ -437,18 +437,6 @@ RSpec.describe ContainerRegistry::Client do
|
|||
|
||||
expect(subject).to eq('tags' => %w[t1 t2])
|
||||
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
|
||||
|
||||
describe '.registry_info' do
|
||||
|
|
|
@ -290,4 +290,13 @@ RSpec.describe AwardEmoji do
|
|||
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
|
||||
|
|
|
@ -186,6 +186,9 @@ RSpec.describe Participable do
|
|||
expect(instance.visible_participants(user1)).to match_array [user1, user2]
|
||||
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
|
||||
|
||||
describe '#participant?' do
|
||||
|
|
|
@ -31,8 +31,8 @@ RSpec.describe IssuablePolicy, models: true do
|
|||
expect(policies).to be_allowed(:resolve_note)
|
||||
end
|
||||
|
||||
it 'allows reading confidential notes' do
|
||||
expect(policies).to be_allowed(:read_confidential_notes)
|
||||
it 'allows reading internal notes' do
|
||||
expect(policies).to be_allowed(:read_internal_note)
|
||||
end
|
||||
|
||||
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(:policies) { described_class.new(user, issue) }
|
||||
|
||||
it 'allows reading confidential notes' do
|
||||
expect(policies).to be_allowed(:read_confidential_notes)
|
||||
it 'allows reading internal notes' do
|
||||
expect(policies).to be_allowed(:read_internal_note)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -145,6 +145,10 @@ RSpec.describe IssuablePolicy, models: true do
|
|||
it 'does not allow timelogs creation' do
|
||||
expect(policies).to be_disallowed(:create_timelog)
|
||||
end
|
||||
|
||||
it 'does not allow reading internal notes' do
|
||||
expect(permissions(guest, issue)).to be_disallowed(:read_internal_note)
|
||||
end
|
||||
end
|
||||
|
||||
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)
|
||||
end
|
||||
|
||||
it 'allows reading confidential notes' do
|
||||
expect(permissions(reporter, issue)).to be_allowed(:read_confidential_notes)
|
||||
it 'allows reading internal notes' do
|
||||
expect(permissions(reporter, issue)).to be_allowed(:read_internal_note)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -188,6 +192,7 @@ RSpec.describe IssuablePolicy, models: true do
|
|||
|
||||
it 'does not allow :read_issuable' do
|
||||
expect(policy).not_to be_allowed(:read_issuable)
|
||||
expect(policy).not_to be_allowed(:read_issuable_participables)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -196,6 +201,7 @@ RSpec.describe IssuablePolicy, models: true do
|
|||
|
||||
it 'allows :read_issuable' do
|
||||
expect(policy).to be_allowed(:read_issuable)
|
||||
expect(policy).to be_allowed(:read_issuable_participables)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -213,6 +219,7 @@ RSpec.describe IssuablePolicy, models: true do
|
|||
|
||||
it 'does not allow :read_issuable' do
|
||||
expect(policy).not_to be_allowed(:read_issuable)
|
||||
expect(policy).not_to be_allowed(:read_issuable_participables)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -221,6 +228,7 @@ RSpec.describe IssuablePolicy, models: true do
|
|||
|
||||
it 'allows :read_issuable' do
|
||||
expect(policy).to be_allowed(:read_issuable)
|
||||
expect(policy).to be_allowed(:read_issuable_participables)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -77,6 +77,48 @@ RSpec.describe API::Ci::ResourceGroups do
|
|||
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
|
||||
subject { put api("/projects/#{project.id}/resource_groups/#{key}", user), params: params }
|
||||
|
||||
|
|
|
@ -686,6 +686,30 @@ RSpec.describe 'getting an issue list for a project' do
|
|||
|
||||
include_examples 'N+1 query check'
|
||||
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
|
||||
|
||||
def issues_ids
|
||||
|
|
|
@ -338,6 +338,27 @@ RSpec.describe 'getting merge request listings nested in a project' do
|
|||
|
||||
include_examples 'N+1 query check'
|
||||
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
|
||||
|
||||
describe 'performance' do
|
||||
|
|
|
@ -6,7 +6,7 @@ RSpec.describe API::Metadata do
|
|||
shared_examples_for 'GET /metadata' do
|
||||
context 'when unauthenticated' do
|
||||
it 'returns authentication error' do
|
||||
get api('/metadata')
|
||||
get api(endpoint)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
end
|
||||
|
@ -16,7 +16,7 @@ RSpec.describe API::Metadata do
|
|||
let(:user) { create(:user) }
|
||||
|
||||
it 'returns the metadata information' do
|
||||
get api('/metadata', user)
|
||||
get api(endpoint, user)
|
||||
|
||||
expect_metadata
|
||||
end
|
||||
|
@ -29,13 +29,13 @@ RSpec.describe API::Metadata do
|
|||
let(:scopes) { %i(api) }
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
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)
|
||||
end
|
||||
|
@ -45,13 +45,13 @@ RSpec.describe API::Metadata do
|
|||
let(:scopes) { %i(read_user) }
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
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)
|
||||
end
|
||||
|
@ -61,7 +61,7 @@ RSpec.describe API::Metadata do
|
|||
let(:scopes) { %i(read_repository) }
|
||||
|
||||
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)
|
||||
end
|
||||
|
@ -76,18 +76,14 @@ RSpec.describe API::Metadata do
|
|||
end
|
||||
end
|
||||
|
||||
context 'with graphql enabled' do
|
||||
before do
|
||||
stub_feature_flags(graphql: true)
|
||||
end
|
||||
describe 'GET /metadata' do
|
||||
let(:endpoint) { '/metadata' }
|
||||
|
||||
include_examples 'GET /metadata'
|
||||
end
|
||||
|
||||
context 'with graphql disabled' do
|
||||
before do
|
||||
stub_feature_flags(graphql: false)
|
||||
end
|
||||
describe 'GET /version' do
|
||||
let(:endpoint) { '/version' }
|
||||
|
||||
include_examples 'GET /metadata'
|
||||
end
|
||||
|
|
|
@ -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
|
|
@ -1,5 +1,5 @@
|
|||
# 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
|
||||
execute_query # "warm up" to prevent undeterministic counts
|
||||
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
|
||||
|
||||
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
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue