Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-12-24 18:13:33 +00:00
parent 7adf802632
commit 7621abc072
20 changed files with 385 additions and 12 deletions

View File

@ -369,6 +369,7 @@ Gitlab/NamespacedClass:
- app/models/wiki_page.rb
- app/models/wiki_page/meta.rb
- app/models/wiki_page/slug.rb
- app/models/work_item.rb
- app/models/x509_certificate.rb
- app/models/x509_commit_signature.rb
- app/models/x509_issuer.rb

View File

@ -291,10 +291,12 @@ class Projects::IssuesController < Projects::ApplicationController
end
def issue_params
params.require(:issue).permit(
all_params = params.require(:issue).permit(
*issue_params_attributes,
sentry_issue_attributes: [:sentry_issue_identifier]
)
clean_params(all_params)
end
def issue_params_attributes
@ -348,6 +350,13 @@ class Projects::IssuesController < Projects::ApplicationController
private
def clean_params(all_params)
issue_type = all_params[:issue_type].to_s
all_params.delete(:issue_type) unless WorkItems::Type.allowed_types_for_issues.include?(issue_type)
all_params
end
def finder_options
options = super

View File

@ -0,0 +1,57 @@
# frozen_string_literal: true
module Mutations
module WorkItems
class Create < BaseMutation
include Mutations::SpamProtection
include FindsProject
graphql_name 'WorkItemCreate'
authorize :create_work_item
argument :description, GraphQL::Types::String,
required: false,
description: copy_field_description(Types::WorkItemType, :description)
argument :project_path, GraphQL::Types::ID,
required: true,
description: 'Full path of the project the work item is associated with.'
argument :title, GraphQL::Types::String,
required: true,
description: copy_field_description(Types::WorkItemType, :title)
argument :work_item_type_id, ::Types::GlobalIDType[::WorkItems::Type],
required: true,
description: 'Global ID of a work item type.'
field :work_item, Types::WorkItemType,
null: true,
description: 'Created work item.'
def resolve(project_path:, **attributes)
project = authorized_find!(project_path)
params = global_id_compatibility_params(attributes).merge(author_id: current_user.id)
spam_params = ::Spam::SpamParams.new_from_request(request: context[:request])
work_item = ::WorkItems::CreateService.new(project: project, current_user: current_user, params: params, spam_params: spam_params).execute
check_spam_action_response!(work_item)
{
work_item: work_item.valid? ? work_item : nil,
errors: errors_on_object(work_item)
}
end
private
def global_id_compatibility_params(params)
# TODO: remove this line when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
params[:work_item_type_id] = ::Types::GlobalIDType[::WorkItems::Type].coerce_isolated_input(params[:work_item_type_id]) if params[:work_item_type_id]
params[:work_item_type_id] = params[:work_item_type_id]&.model_id
params
end
end
end
end

View File

@ -123,6 +123,7 @@ module Types
mount_mutation Mutations::Packages::Destroy
mount_mutation Mutations::Packages::DestroyFile
mount_mutation Mutations::Echo
mount_mutation Mutations::WorkItems::Create, feature_flag: :work_items
end
end

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
module Types
class WorkItemType < BaseObject
graphql_name 'WorkItem'
authorize :read_issue
field :description, GraphQL::Types::String, null: true,
description: 'Description of the work item.'
field :id, Types::GlobalIDType[::WorkItem], null: false,
description: 'Global ID of the work item.'
field :iid, GraphQL::Types::ID, null: false,
description: 'Internal ID of the work item.'
field :title, GraphQL::Types::String, null: false,
description: 'Title of the work item.'
field :work_item_type, Types::WorkItems::TypeType, null: false,
description: 'Type assigned to the work item.'
markdown_field :title_html, null: true
markdown_field :description_html, null: true
end
end

View File

@ -603,6 +603,11 @@ class Issue < ApplicationRecord
author&.banned?
end
# Necessary until all issues are backfilled and we add a NOT NULL constraint on the DB
def work_item_type
super || WorkItems::Type.default_by_type(issue_type)
end
private
def spammable_attribute_changed?

6
app/models/work_item.rb Normal file
View File

@ -0,0 +1,6 @@
# frozen_string_literal: true
class WorkItem < Issue
self.table_name = 'issues'
self.inheritance_column = :_type_disabled
end

View File

@ -258,6 +258,11 @@ class ProjectPolicy < BasePolicy
rule { can?(:reporter_access) & can?(:create_issue) }.enable :create_incident
rule { can?(:guest_access) & can?(:create_issue) }.policy do
enable :create_task
enable :create_work_item
end
# These abilities are not allowed to admins that are not members of the project,
# that's why they are defined separately.
rule { guest & can?(:download_code) }.enable :build_download_code

View File

@ -7,7 +7,7 @@ module Issues
def execute
filter_resolve_discussion_params
@issue = project.issues.new(issue_params).tap do |issue|
@issue = model_klass.new(issue_params.merge(project: project)).tap do |issue|
ensure_milestone_available(issue)
end
end
@ -62,16 +62,25 @@ module Issues
def issue_params
@issue_params ||= build_issue_params
# If :issue_type is nil then params[:issue_type] was either nil
# or not permitted. Either way, the :issue_type will default
# to the column default of `issue`. And that means we need to
# ensure the work_item_type_id is set
@issue_params[:work_item_type_id] = get_work_item_type_id(@issue_params[:issue_type])
if @issue_params[:work_item_type].present?
@issue_params[:issue_type] = @issue_params[:work_item_type].base_type
else
# If :issue_type is nil then params[:issue_type] was either nil
# or not permitted. Either way, the :issue_type will default
# to the column default of `issue`. And that means we need to
# ensure the work_item_type_id is set
@issue_params[:work_item_type_id] = get_work_item_type_id(@issue_params[:issue_type])
end
@issue_params
end
private
def model_klass
::Issue
end
def allowed_issue_params
allowed_params = [
:title,
@ -79,8 +88,11 @@ module Issues
:confidential
]
params[:work_item_type] = WorkItems::Type.find_by(id: params[:work_item_type_id]) if params[:work_item_type_id].present? # rubocop: disable CodeReuse/ActiveRecord
allowed_params << :milestone_id if can?(current_user, :admin_issue, project)
allowed_params << :issue_type if create_issue_type_allowed?(project, params[:issue_type])
allowed_params << :work_item_type if create_issue_type_allowed?(project, params[:work_item_type]&.base_type)
params.slice(*allowed_params)
end

View File

@ -12,13 +12,14 @@ module Issues
# spam_checking is likely to be necessary. However, if there is not a request available in scope
# in the caller (for example, an issue created via email) and the required arguments to the
# SpamParams constructor are not otherwise available, spam_params: must be explicitly passed as nil.
def initialize(project:, current_user: nil, params: {}, spam_params:)
def initialize(project:, current_user: nil, params: {}, spam_params:, build_service: nil)
super(project: project, current_user: current_user, params: params)
@spam_params = spam_params
@build_service = build_service || BuildService.new(project: project, current_user: current_user, params: params)
end
def execute(skip_system_notes: false)
@issue = BuildService.new(project: project, current_user: current_user, params: params).execute
@issue = @build_service.execute
filter_resolve_discussion_params

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
module WorkItems
class BuildService < ::Issues::BuildService
private
def model_klass
::WorkItem
end
end
end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
module WorkItems
class CreateService
def initialize(project:, current_user: nil, params: {}, spam_params:)
@create_service = ::Issues::CreateService.new(
project: project,
current_user: current_user,
params: params,
spam_params: spam_params,
build_service: ::WorkItems::BuildService.new(project: project, current_user: current_user, params: params)
)
end
def execute
@create_service.execute
end
end
end

View File

@ -4951,6 +4951,30 @@ Input type: `VulnerabilityRevertToDetectedInput`
| <a id="mutationvulnerabilityreverttodetectederrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationvulnerabilityreverttodetectedvulnerability"></a>`vulnerability` | [`Vulnerability`](#vulnerability) | Vulnerability after revert. |
### `Mutation.workItemCreate`
Available only when feature flag `work_items` is enabled. This flag is disabled by default, because the feature is experimental and is subject to change without notice.
Input type: `WorkItemCreateInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationworkitemcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationworkitemcreatedescription"></a>`description` | [`String`](#string) | Description of the work item. |
| <a id="mutationworkitemcreateprojectpath"></a>`projectPath` | [`ID!`](#id) | Full path of the project the work item is associated with. |
| <a id="mutationworkitemcreatetitle"></a>`title` | [`String!`](#string) | Title of the work item. |
| <a id="mutationworkitemcreateworkitemtypeid"></a>`workItemTypeId` | [`WorkItemsTypeID!`](#workitemstypeid) | Global ID of a work item type. |
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationworkitemcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationworkitemcreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationworkitemcreateworkitem"></a>`workItem` | [`WorkItem`](#workitem) | Created work item. |
## Connections
Some types in our schema are `Connection` types - they represent a paginated
@ -16019,6 +16043,20 @@ Represents vulnerability letter grades with associated projects.
| <a id="vulnerableprojectsbygradegrade"></a>`grade` | [`VulnerabilityGrade!`](#vulnerabilitygrade) | Grade based on the highest severity vulnerability present. |
| <a id="vulnerableprojectsbygradeprojects"></a>`projects` | [`ProjectConnection!`](#projectconnection) | Projects within this grade. (see [Connections](#connections)) |
### `WorkItem`
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="workitemdescription"></a>`description` | [`String`](#string) | Description of the work item. |
| <a id="workitemdescriptionhtml"></a>`descriptionHtml` | [`String`](#string) | The GitLab Flavored Markdown rendering of `description`. |
| <a id="workitemid"></a>`id` | [`WorkItemID!`](#workitemid) | Global ID of the work item. |
| <a id="workitemiid"></a>`iid` | [`ID!`](#id) | Internal ID of the work item. |
| <a id="workitemtitle"></a>`title` | [`String!`](#string) | Title of the work item. |
| <a id="workitemtitlehtml"></a>`titleHtml` | [`String`](#string) | The GitLab Flavored Markdown rendering of `title`. |
| <a id="workitemworkitemtype"></a>`workItemType` | [`WorkItemType!`](#workitemtype) | Type assigned to the work item. |
### `WorkItemType`
#### Fields
@ -18121,6 +18159,12 @@ A `VulnerabilityID` is a global ID. It is encoded as a string.
An example `VulnerabilityID` is: `"gid://gitlab/Vulnerability/1"`.
### `WorkItemID`
A `WorkItemID` is a global ID. It is encoded as a string.
An example `WorkItemID` is: `"gid://gitlab/WorkItem/1"`.
### `WorkItemsTypeID`
A `WorkItemsTypeID` is a global ID. It is encoded as a string.

View File

@ -238,6 +238,17 @@ RSpec.describe Issue do
end
end
# TODO: Remove when NOT NULL constraint is added to the relationship
describe '#work_item_type' do
let(:issue) { create(:issue, :incident, project: reusable_project, work_item_type: nil) }
it 'returns a default type if the legacy issue does not have a work item type associated yet' do
expect(issue.work_item_type_id).to be_nil
expect(issue.issue_type).to eq('incident')
expect(issue.work_item_type).to eq(WorkItems::Type.default_by_type(:incident))
end
end
describe '#sort' do
let(:project) { reusable_project }

View File

@ -61,7 +61,7 @@ RSpec.describe ProjectPolicy do
end
it 'does not include the issues permissions' do
expect_disallowed :read_issue, :read_issue_iid, :create_issue, :update_issue, :admin_issue, :create_incident
expect_disallowed :read_issue, :read_issue_iid, :create_issue, :update_issue, :admin_issue, :create_incident, :create_work_item, :create_task
end
it 'disables boards and lists permissions' do
@ -73,7 +73,7 @@ RSpec.describe ProjectPolicy do
it 'does not include the issues permissions' do
create(:jira_integration, project: project)
expect_disallowed :read_issue, :read_issue_iid, :create_issue, :update_issue, :admin_issue, :create_incident
expect_disallowed :read_issue, :read_issue_iid, :create_issue, :update_issue, :admin_issue, :create_incident, :create_work_item, :create_task
end
end
end

View File

@ -0,0 +1,63 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Create a work item' do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
let_it_be(:developer) { create(:user).tap { |user| project.add_developer(user) } }
let(:input) do
{
'title' => 'new title',
'description' => 'new description',
'workItemTypeId' => WorkItems::Type.default_by_type(:task).to_global_id.to_s
}
end
let(:mutation) { graphql_mutation(:workItemCreate, input.merge('projectPath' => project.full_path)) }
let(:mutation_response) { graphql_mutation_response(:work_item_create) }
context 'the user is not allowed to create a work item' do
let(:current_user) { create(:user) }
it_behaves_like 'a mutation that returns a top-level access error'
end
context 'when user has permissions to create a work item' do
let(:current_user) { developer }
it 'creates the work item' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
end.to change(WorkItem, :count).by(1)
created_work_item = WorkItem.last
expect(response).to have_gitlab_http_status(:success)
expect(created_work_item.issue_type).to eq('task')
expect(created_work_item.work_item_type.base_type).to eq('task')
expect(mutation_response['workItem']).to include(
input.except('workItemTypeId').merge(
'id' => created_work_item.to_global_id.to_s,
'workItemType' => hash_including('name' => 'Task')
)
)
end
it_behaves_like 'has spam protection' do
let(:mutation_class) { ::Mutations::WorkItems::Create }
end
context 'when the work_items feature flag is disabled' do
before do
stub_feature_flags(work_items: false)
end
it_behaves_like 'a mutation that returns top-level errors',
errors: ["Field 'workItemCreate' doesn't exist on type 'Mutation'", "Variable $workItemCreateInput is declared by anonymous mutation but not used"]
end
end
end

View File

@ -61,6 +61,7 @@ RSpec.describe Issues::CreateService do
expect(Issuable::CommonSystemNotesService).to receive_message_chain(:new, :execute)
expect(issue).to be_persisted
expect(issue).to be_a(::Issue)
expect(issue.title).to eq('Awesome issue')
expect(issue.assignees).to eq([assignee])
expect(issue.labels).to match_array(labels)
@ -69,6 +70,18 @@ RSpec.describe Issues::CreateService do
expect(issue.work_item_type.base_type).to eq('issue')
end
context 'when a build_service is provided' do
let(:issue) { described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params, build_service: build_service).execute }
let(:issue_from_builder) { WorkItem.new(project: project, title: 'Issue from builder') }
let(:build_service) { double(:build_service, execute: issue_from_builder) }
it 'uses the provided service to build the issue' do
expect(issue).to be_persisted
expect(issue).to be_a(WorkItem)
end
end
context 'when skip_system_notes is true' do
let(:issue) { described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute(skip_system_notes: true) }

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe WorkItems::BuildService do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:guest) { create(:user) }
let(:user) { guest }
before_all do
project.add_guest(guest)
end
describe '#execute' do
subject { described_class.new(project: project, current_user: user, params: {}).execute }
it { is_expected.to be_a(::WorkItem) }
end
end

View File

@ -0,0 +1,72 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe WorkItems::CreateService do
include AfterNextHelpers
let_it_be(:group) { create(:group) }
let_it_be_with_reload(:project) { create(:project, group: group) }
let_it_be(:user) { create(:user) }
let(:spam_params) { double }
describe '#execute' do
let(:work_item) { described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute }
before do
stub_spam_services
end
context 'when params are valid' do
before_all do
project.add_guest(user)
end
let(:opts) do
{
title: 'Awesome work_item',
description: 'please fix'
}
end
it 'created instance is a WorkItem' do
expect(Issuable::CommonSystemNotesService).to receive_message_chain(:new, :execute)
expect(work_item).to be_persisted
expect(work_item).to be_a(::WorkItem)
expect(work_item.title).to eq('Awesome work_item')
expect(work_item.description).to eq('please fix')
expect(work_item.work_item_type.base_type).to eq('issue')
end
end
context 'checking spam' do
let(:params) do
{
title: 'Spam work_item'
}
end
subject do
described_class.new(project: project, current_user: user, params: params, spam_params: spam_params)
end
it 'executes SpamActionService' do
expect_next_instance_of(
Spam::SpamActionService,
{
spammable: kind_of(WorkItem),
spam_params: spam_params,
user: an_instance_of(User),
action: :create
}
) do |instance|
expect(instance).to receive(:execute)
end
subject.execute
end
end
end
end

View File

@ -8,7 +8,7 @@
# There must be a method or let called `mutation` defined that executes
# the mutation.
RSpec.shared_examples 'a mutation that returns top-level errors' do |errors: []|
let(:match_errors) { eq(errors) }
let(:match_errors) { match_array(errors) }
it do
post_graphql_mutation(mutation, current_user: current_user)