Commit 25b287bf authored by Mario Celi's avatar Mario Celi

Add ability to set iteration on issue creation via GraphQL API

An iteration can be assigned to a created issue via the GraphQL API
by providing one of the following:

- Specific iteration global ID (iterationId).
- iterationWildcardId (only CURRENT is supported).
- IterationCadenceId is also required when iterationWildcardId
  is provided.

Changelog: added
EE: true
parent f8b652b9
...@@ -71,7 +71,7 @@ module Mutations ...@@ -71,7 +71,7 @@ module Mutations
def resolve(project_path:, **attributes) def resolve(project_path:, **attributes)
project = authorized_find!(project_path) project = authorized_find!(project_path)
params = build_create_issue_params(attributes.merge(author_id: current_user.id)) params = build_create_issue_params(attributes.merge(author_id: current_user.id), project)
spam_params = ::Spam::SpamParams.new_from_request(request: context[:request]) spam_params = ::Spam::SpamParams.new_from_request(request: context[:request])
issue = ::Issues::CreateService.new(project: project, current_user: current_user, params: params, spam_params: spam_params).execute issue = ::Issues::CreateService.new(project: project, current_user: current_user, params: params, spam_params: spam_params).execute
...@@ -88,7 +88,8 @@ module Mutations ...@@ -88,7 +88,8 @@ module Mutations
private private
def build_create_issue_params(params) # _project argument is unused here, but it is necessary on the EE version of the method
def build_create_issue_params(params, _project)
params[:milestone_id] &&= params[:milestone_id]&.model_id params[:milestone_id] &&= params[:milestone_id]&.model_id
params[:assignee_ids] &&= params[:assignee_ids].map { |assignee_id| assignee_id&.model_id } params[:assignee_ids] &&= params[:assignee_ids].map { |assignee_id| assignee_id&.model_id }
params[:label_ids] &&= params[:label_ids].map { |label_id| label_id&.model_id } params[:label_ids] &&= params[:label_ids].map { |label_id| label_id&.model_id }
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
# Base class, scoped by project # Base class, scoped by project
class BaseProjectService < ::BaseContainerService class BaseProjectService < ::BaseContainerService
include ::Gitlab::Utils::StrongMemoize
attr_accessor :project attr_accessor :project
def initialize(project:, current_user: nil, params: {}) def initialize(project:, current_user: nil, params: {})
...@@ -11,4 +13,12 @@ class BaseProjectService < ::BaseContainerService ...@@ -11,4 +13,12 @@ class BaseProjectService < ::BaseContainerService
end end
delegate :repository, to: :project delegate :repository, to: :project
private
def project_group
strong_memoize(:project_group) do
project.group
end
end
end end
...@@ -1267,6 +1267,9 @@ Input type: `CreateIssueInput` ...@@ -1267,6 +1267,9 @@ Input type: `CreateIssueInput`
| <a id="mutationcreateissueepicid"></a>`epicId` | [`EpicID`](#epicid) | ID of an epic to associate the issue with. | | <a id="mutationcreateissueepicid"></a>`epicId` | [`EpicID`](#epicid) | ID of an epic to associate the issue with. |
| <a id="mutationcreateissuehealthstatus"></a>`healthStatus` | [`HealthStatus`](#healthstatus) | Desired health status. | | <a id="mutationcreateissuehealthstatus"></a>`healthStatus` | [`HealthStatus`](#healthstatus) | Desired health status. |
| <a id="mutationcreateissueiid"></a>`iid` | [`Int`](#int) | IID (internal ID) of a project issue. Only admins and project owners can modify. | | <a id="mutationcreateissueiid"></a>`iid` | [`Int`](#int) | IID (internal ID) of a project issue. Only admins and project owners can modify. |
| <a id="mutationcreateissueiterationcadenceid"></a>`iterationCadenceId` | [`IterationsCadenceID`](#iterationscadenceid) | Global iteration cadence ID. Required when `iterationWildcardId` is provided. |
| <a id="mutationcreateissueiterationid"></a>`iterationId` | [`IterationID`](#iterationid) | Global iteration ID. Mutually exlusive argument with `iterationWildcardId`. |
| <a id="mutationcreateissueiterationwildcardid"></a>`iterationWildcardId` | [`IssueCreationIterationWildcardId`](#issuecreationiterationwildcardid) | Iteration wildcard ID. Supported values are: `CURRENT`. Mutually exclusive argument with `iterationId`. iterationCadenceId also required when this argument is provided. |
| <a id="mutationcreateissuelabelids"></a>`labelIds` | [`[LabelID!]`](#labelid) | IDs of labels to be added to the issue. | | <a id="mutationcreateissuelabelids"></a>`labelIds` | [`[LabelID!]`](#labelid) | IDs of labels to be added to the issue. |
| <a id="mutationcreateissuelabels"></a>`labels` | [`[String!]`](#string) | Labels of the issue. | | <a id="mutationcreateissuelabels"></a>`labels` | [`[String!]`](#string) | Labels of the issue. |
| <a id="mutationcreateissuelocked"></a>`locked` | [`Boolean`](#boolean) | Indicates discussion is locked on the issue. | | <a id="mutationcreateissuelocked"></a>`locked` | [`Boolean`](#boolean) | Indicates discussion is locked on the issue. |
...@@ -15808,6 +15811,14 @@ State of a GitLab issue or merge request. ...@@ -15808,6 +15811,14 @@ State of a GitLab issue or merge request.
| <a id="issuablestatelocked"></a>`locked` | Discussion has been locked. | | <a id="issuablestatelocked"></a>`locked` | Discussion has been locked. |
| <a id="issuablestateopened"></a>`opened` | In open state. | | <a id="issuablestateopened"></a>`opened` | In open state. |
### `IssueCreationIterationWildcardId`
Iteration ID wildcard values for issue creation.
| Value | Description |
| ----- | ----------- |
| <a id="issuecreationiterationwildcardidcurrent"></a>`CURRENT` | Current iteration. |
### `IssueSort` ### `IssueSort`
Values for sorting issues. Values for sorting issues.
......
...@@ -116,9 +116,7 @@ module EE ...@@ -116,9 +116,7 @@ module EE
{ {
parent: params.parent, parent: params.parent,
include_ancestors: true, include_ancestors: true,
state: 'opened', iteration_wildcard_id: ::Iteration::Predefined::Current.title
start_date: Date.today,
end_date: Date.today
} }
end end
end end
......
...@@ -23,6 +23,8 @@ class IterationsFinder ...@@ -23,6 +23,8 @@ class IterationsFinder
def execute(skip_authorization: false) def execute(skip_authorization: false)
@skip_authorization = skip_authorization @skip_authorization = skip_authorization
handle_wildcard_params
items = Iteration.all items = Iteration.all
items = by_id(items) items = by_id(items)
items = by_iid(items) items = by_iid(items)
...@@ -40,6 +42,14 @@ class IterationsFinder ...@@ -40,6 +42,14 @@ class IterationsFinder
attr_reader :skip_authorization attr_reader :skip_authorization
# wildcard params do not override other explicitely given params
def handle_wildcard_params
if params[:iteration_wildcard_id] && params[:iteration_wildcard_id].casecmp?(::Iteration::Predefined::Current.title)
params[:start_date] ||= Date.today
params[:end_date] ||= Date.today
end
end
def by_groups(items) def by_groups(items)
return Iteration.none unless skip_authorization || Ability.allowed?(current_user, :read_iteration, params[:parent]) return Iteration.none unless skip_authorization || Ability.allowed?(current_user, :read_iteration, params[:parent])
......
...@@ -13,6 +13,17 @@ module EE ...@@ -13,6 +13,17 @@ module EE
argument :epic_id, ::Types::GlobalIDType[::Epic], argument :epic_id, ::Types::GlobalIDType[::Epic],
required: false, required: false,
description: 'ID of an epic to associate the issue with.' description: 'ID of an epic to associate the issue with.'
argument :iteration_id, ::Types::GlobalIDType[::Iteration],
required: false,
description: 'Global iteration ID. Mutually exlusive argument with `iterationWildcardId`.'
argument :iteration_wildcard_id, ::Types::IssueCreationIterationWildcardIdEnum,
required: false,
description: 'Iteration wildcard ID. Supported values are: `CURRENT`.' \
' Mutually exclusive argument with `iterationId`.' \
' iterationCadenceId also required when this argument is provided.'
argument :iteration_cadence_id, ::Types::GlobalIDType[::Iterations::Cadence],
required: false,
description: 'Global iteration cadence ID. Required when `iterationWildcardId` is provided.'
end end
override :resolve override :resolve
...@@ -20,18 +31,53 @@ module EE ...@@ -20,18 +31,53 @@ module EE
super super
rescue ActiveRecord::RecordNotFound => e rescue ActiveRecord::RecordNotFound => e
{ errors: [e.message], issue: nil } { errors: [e.message], issue: nil }
rescue ::Issues::BaseService::IterationAssignmentError => e
raise(
::Gitlab::Graphql::Errors::ArgumentError,
transform_field_names(e.message)
)
end end
private private
override :build_create_issue_params override :build_create_issue_params
def build_create_issue_params(params) def build_create_issue_params(params, project)
# TODO: remove this line when the compatibility layer is removed # TODO: remove this line when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
params[:epic_id] = ::Types::GlobalIDType[::Epic].coerce_isolated_input(params[:epic_id]) if params[:epic_id] params[:epic_id] = ::Types::GlobalIDType[::Epic].coerce_isolated_input(params[:epic_id]) if params[:epic_id]
params[:epic_id] = params[:epic_id]&.model_id if params.key?(:epic_id) params[:epic_id] = params[:epic_id]&.model_id if params.key?(:epic_id)
super(params) handle_iteration_params(params, project)
super
end
def handle_iteration_params(params, project)
group = project.group
return unless group && group.licensed_feature_available?(:iterations)
# TODO: remove this line when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
params[:iteration_id] = ::Types::GlobalIDType[::Iteration].coerce_isolated_input(params[:iteration_id]) if params[:iteration_id]
params[:iteration_id] = params[:iteration_id]&.model_id
# TODO: remove this line when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
params[:iteration_cadence_id] = ::Types::GlobalIDType[::Iterations::Cadence].coerce_isolated_input(params[:iteration_cadence_id]) if params[:iteration_cadence_id]
params[:iteration_cadence_id] = params[:iteration_cadence_id]&.model_id
end
def name_mappings
{
'iteration_wildcard_id' => 'iterationWildcardId',
'iteration_cadence_id' => 'iterationCadenceId',
'iteration_id' => 'iterationId'
}
end
def transform_field_names(message)
name_mappings.reduce(message) do |transformed_message, (k, v)|
transformed_message.gsub(k, v)
end
end end
end end
end end
......
# frozen_string_literal: true
module Types
class IssueCreationIterationWildcardIdEnum < BaseEnum
graphql_name 'IssueCreationIterationWildcardId'
description 'Iteration ID wildcard values for issue creation'
value 'CURRENT', 'Current iteration.'
end
end
...@@ -21,6 +21,10 @@ module EE ...@@ -21,6 +21,10 @@ module EE
params.delete(:health_status) params.delete(:health_status)
end end
# Filter these iteration params unconditionally as they do not exist on the model.
# They must be used before reaching this filter if present.
[:iteration_wildcard_id, :iteration_cadence_id, :iteration_id].each { |iteration_param| params.delete(iteration_param) }
super super
end end
......
...@@ -5,7 +5,8 @@ module EE ...@@ -5,7 +5,8 @@ module EE
module BaseService module BaseService
extend ::Gitlab::Utils::Override extend ::Gitlab::Utils::Override
class EpicAssignmentError < ::ArgumentError; end EpicAssignmentError = Class.new(::ArgumentError)
IterationAssignmentError = Class.new(StandardError)
def filter_epic(issue) def filter_epic(issue)
return unless epic_param_present? return unless epic_param_present?
...@@ -107,6 +108,42 @@ module EE ...@@ -107,6 +108,42 @@ module EE
raise EpicAssignmentError, result[:message] raise EpicAssignmentError, result[:message]
end end
end end
private
def validate_iteration_params!(iteration_params)
if iteration_params[:iteration_wildcard_id].present? && iteration_params[:iteration_cadence_id].blank?
raise IterationAssignmentError, 'iteration_cadence_id is required when iteration_wildcard_id is provided.'
end
if [iteration_params[:iteration_id], iteration_params[:iteration_wildcard_id]].all?(&:present?)
raise IterationAssignmentError, 'Incompatible arguments: iteration_id, iteration_wildcard_id.'
end
end
def find_iteration!(iteration_params, group)
validate_iteration_params!(iteration_params)
# converts params to keys the finder understands
finder_params = iteration_params.slice(:iteration_wildcard_id).merge(parent: group, include_ancestors: true)
finder_params[:id] = iteration_params[:iteration_id]
finder_params[:iteration_cadence_ids] = iteration_params[:iteration_cadence_id]
iteration = IterationsFinder.new(current_user, finder_params.compact).execute.first
return unless iteration && current_user.can?(:read_iteration, iteration)
iteration
end
def process_iteration_id
return unless project_group&.licensed_feature_available?(:iterations)
iteration_params = params.slice(:iteration_wildcard_id, :iteration_cadence_id, :iteration_id)
iteration = find_iteration!(iteration_params, project_group)
params[:iteration] = iteration if iteration
end
end end
end end
end end
...@@ -5,6 +5,13 @@ module EE ...@@ -5,6 +5,13 @@ module EE
module CreateService module CreateService
extend ::Gitlab::Utils::Override extend ::Gitlab::Utils::Override
override :create
def create(issuable, skip_system_notes: false)
process_iteration_id
super
end
override :filter_params override :filter_params
def filter_params(issue) def filter_params(issue)
filter_epic(issue) filter_epic(issue)
......
...@@ -67,6 +67,28 @@ RSpec.describe IterationsFinder do ...@@ -67,6 +67,28 @@ RSpec.describe IterationsFinder do
it 'returns iterations for groups' do it 'returns iterations for groups' do
expect(subject).to contain_exactly(closed_iteration, started_group_iteration, upcoming_group_iteration) expect(subject).to contain_exactly(closed_iteration, started_group_iteration, upcoming_group_iteration)
end end
context 'with filters' do
context 'by iteration_wildcard_id' do
let_it_be(:started_group_iteration2) { create(:current_iteration, :skip_future_date_validation, iterations_cadence: iteration_cadence1, group: iteration_cadence1.group, title: 'one test', start_date: 1.day.ago, due_date: Date.today) }
before do
params[:iteration_wildcard_id] = 'CURRENT'
end
it 'returns CURRENT iterations without ancestors' do
expect(subject).to contain_exactly(started_group_iteration, started_group_iteration2)
end
context 'when iteration_cadence_id is provided' do
it 'returns CURRENT iteration for the given cadence' do
params[:iteration_cadence_ids] = iteration_cadence1.id
expect(subject).to contain_exactly(started_group_iteration2)
end
end
end
end
end end
context 'iterations for project with ancestors' do context 'iterations for project with ancestors' do
...@@ -134,6 +156,22 @@ RSpec.describe IterationsFinder do ...@@ -134,6 +156,22 @@ RSpec.describe IterationsFinder do
expect(subject).to contain_exactly(closed_iteration, started_group_iteration, upcoming_group_iteration) expect(subject).to contain_exactly(closed_iteration, started_group_iteration, upcoming_group_iteration)
end end
context 'by iteration_wildcard_id' do
before do
params[:iteration_wildcard_id] = 'CURRENT'
end
it 'returns CURRENT iterations' do
expect(subject).to contain_exactly(root_group_iteration, started_group_iteration)
end
it 'returns CURRENT iteration for the specified cadence' do
params[:iteration_cadence_ids] = started_group_iteration.iterations_cadence.id
expect(subject).to contain_exactly(started_group_iteration)
end
end
context 'by timeframe' do context 'by timeframe' do
it 'returns iterations with start_date and due_date between timeframe' do it 'returns iterations with start_date and due_date between timeframe' do
params.merge!(start_date: 1.day.ago, end_date: 3.days.from_now) params.merge!(start_date: 1.day.ago, end_date: 3.days.from_now)
......
...@@ -7,6 +7,8 @@ RSpec.describe Mutations::Issues::Create do ...@@ -7,6 +7,8 @@ RSpec.describe Mutations::Issues::Create do
let_it_be(:group) { create(:group) } let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) } let_it_be(:project) { create(:project, group: group) }
let_it_be(:cadence1) { create(:iterations_cadence, group: group) }
let_it_be(:current_iteration) { create(:iteration, group: group, iterations_cadence: cadence1, start_date: 2.days.ago, due_date: 5.days.from_now) }
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:assignee1) { create(:user) } let_it_be(:assignee1) { create(:user) }
let_it_be(:assignee2) { create(:user) } let_it_be(:assignee2) { create(:user) }
...@@ -31,29 +33,119 @@ RSpec.describe Mutations::Issues::Create do ...@@ -31,29 +33,119 @@ RSpec.describe Mutations::Issues::Create do
end end
let(:mutation_params) do let(:mutation_params) do
inputs.merge(expected_attributes) inputs.merge(expected_attributes).merge(additional_attributes)
end end
let(:additional_attributes) { {} }
let(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) } let(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
let(:mutated_issue) { subject[:issue] } let(:mutated_issue) { resolved_mutation[:issue] }
before_all do
project.add_guest(assignee1)
project.add_guest(assignee2)
end
specify { expect(described_class).to require_graphql_authorizations(:create_issue) } specify { expect(described_class).to require_graphql_authorizations(:create_issue) }
describe '#resolve' do describe '#resolve' do
before do before do
project.add_guest(assignee1)
project.add_guest(assignee2)
stub_licensed_features(issuable_health_status: true) stub_licensed_features(issuable_health_status: true)
stub_spam_services stub_spam_services
end end
subject { mutation.resolve(**mutation_params) } subject(:resolved_mutation) { mutation.resolve(**mutation_params) }
context 'when user can create issues' do context 'when user can create issues' do
before do before_all do
group.add_developer(user) group.add_developer(user)
end end
context 'when iterations are available' do
let_it_be(:past_iteration) { create(:iteration, group: group, iterations_cadence: cadence1, start_date: 9.days.ago, due_date: 3.days.ago) }
let_it_be(:future_iteration) { create(:iteration, group: group, iterations_cadence: cadence1, start_date: 6.days.from_now, due_date: 9.days.from_now) }
before do
stub_licensed_features(iterations: true)
end
context 'when iteration_id is provided' do
let(:additional_attributes) { { iteration_id: past_iteration.to_global_id } }
it 'is successful, and assigns the current iteration to the issue' do
expect(resolved_mutation[:errors]).to be_empty
expect(mutated_issue).to have_attributes(iteration: past_iteration)
end
context 'when iteration_wildcard_id is provided' do
let(:additional_attributes) { { iteration_id: past_iteration.to_global_id, iteration_wildcard_id: 'CURRENT', iteration_cadence_id: cadence1.to_global_id } }
it 'raises a mutually exclusive argument error' do
expect { resolved_mutation }.to raise_error(
::Gitlab::Graphql::Errors::ArgumentError,
'Incompatible arguments: iterationId, iterationWildcardId.'
)
end
end
context 'when iteration cadences feature flag is disabled' do
before do
stub_feature_flags(iteration_cadences: false)
end
it 'is successful, and assigns the current iteration to the issue' do
expect(resolved_mutation[:errors]).to be_empty
expect(mutated_issue).to have_attributes(iteration: past_iteration)
end
end
end
context 'when iteration_wildcard_id is CURRENT' do
let(:additional_attributes) { { iteration_wildcard_id: 'CURRENT' } }
context 'when iteration_cadence_id is provided' do
let(:additional_attributes) { { iteration_wildcard_id: 'CURRENT', iteration_cadence_id: cadence1.to_global_id } }
it 'is successful, and assigns the current iteration to the issue' do
expect(resolved_mutation[:errors]).to be_empty
expect(mutated_issue).to have_attributes(iteration: current_iteration)
end
end
context 'when iteration_cadence_id is not provided' do
it 'always requires iteration cadence id when wildcard is provided' do
expect { resolved_mutation }.to raise_error(
::Gitlab::Graphql::Errors::ArgumentError,
'iterationCadenceId is required when iterationWildcardId is provided.'
)
end
end
end
end
context 'when iterations are not available' do
before do
stub_licensed_features(iterations: false)
end
context 'when iteration_wildcard_id is provided' do
let(:additional_attributes) { { iteration_wildcard_id: 'CURRENT' } }
it 'is successful, but it does not add the iteration' do
expect(resolved_mutation[:errors]).to be_empty
expect(mutated_issue).to have_attributes(iteration: nil)
end
end
context 'when iteration_id is provided' do
let(:additional_attributes) { { iteration_id: current_iteration.to_global_id } }
it 'is successful, but it does not add the iteration' do
expect(resolved_mutation[:errors]).to be_empty
expect(mutated_issue).to have_attributes(iteration: nil)
end
end
end
it 'creates issue with correct EE values' do it 'creates issue with correct EE values' do
expect(mutated_issue).to have_attributes(expected_attributes) expect(mutated_issue).to have_attributes(expected_attributes)
expect(mutated_issue.assignees.pluck(:id)).to eq([assignee1.id, assignee2.id]) expect(mutated_issue.assignees.pluck(:id)).to eq([assignee1.id, assignee2.id])
...@@ -73,7 +165,7 @@ RSpec.describe Mutations::Issues::Create do ...@@ -73,7 +165,7 @@ RSpec.describe Mutations::Issues::Create do
end end
it 'is successful, and assigns the issue to the epic' do it 'is successful, and assigns the issue to the epic' do
expect(subject[:errors]).to be_empty expect(resolved_mutation[:errors]).to be_empty
expect(mutated_issue).to have_attributes(epic: epic) expect(mutated_issue).to have_attributes(epic: epic)
end end
...@@ -83,7 +175,7 @@ RSpec.describe Mutations::Issues::Create do ...@@ -83,7 +175,7 @@ RSpec.describe Mutations::Issues::Create do
it 'is successful, but it does not add the epic' do it 'is successful, but it does not add the epic' do
project.add_developer(user) project.add_developer(user)
expect(subject[:errors]).to be_empty expect(resolved_mutation[:errors]).to be_empty
expect(mutated_issue).not_to have_attributes(epic: epic) expect(mutated_issue).not_to have_attributes(epic: epic)
end end
end end
...@@ -91,7 +183,7 @@ RSpec.describe Mutations::Issues::Create do ...@@ -91,7 +183,7 @@ RSpec.describe Mutations::Issues::Create do
context 'epics are unavailable' do context 'epics are unavailable' do
it 'is unsuccessful' do it 'is unsuccessful' do
expect(subject[:errors]).to contain_exactly("Couldn't find Epic") expect(resolved_mutation[:errors]).to contain_exactly("Couldn't find Epic")
end end
it 'does not create an issue' do it 'does not create an issue' do
......
...@@ -6,13 +6,17 @@ RSpec.describe 'Create an issue' do ...@@ -6,13 +6,17 @@ RSpec.describe 'Create an issue' do
include GraphqlHelpers include GraphqlHelpers
let_it_be(:current_user) { create(:user) } let_it_be(:current_user) { create(:user) }
let_it_be(:project) { create(:project) } let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, namespace: group) }
let_it_be(:current_iteration) { create(:iteration, group: group, start_date: 2.days.ago, due_date: 10.days.from_now) }
let(:input) do let(:input) do
{ {
'title' => 'new title', 'title' => 'new title',
'weight' => 2, 'weight' => 2,
'healthStatus' => 'atRisk' 'healthStatus' => 'atRisk',
'iterationWildcardId' => 'CURRENT',
'iterationCadenceId' => current_iteration.iterations_cadence.to_global_id.to_s
} }
end end
...@@ -21,14 +25,53 @@ RSpec.describe 'Create an issue' do ...@@ -21,14 +25,53 @@ RSpec.describe 'Create an issue' do
let(:mutation_response) { graphql_mutation_response(:create_issue) } let(:mutation_response) { graphql_mutation_response(:create_issue) }
before do before do
stub_licensed_features(issuable_health_status: true) stub_licensed_features(issuable_health_status: true, iterations: true)
project.add_developer(current_user) group.add_developer(current_user)
end end
it 'creates the issue' do it 'creates the issue' do
post_graphql_mutation(mutation, current_user: current_user) post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success) expect(response).to have_gitlab_http_status(:success)
expect(mutation_response['issue']).to include(input) expect(mutation_response['issue']).to include(input.except('iterationWildcardId', 'iterationCadenceId'))
expect(mutation_response['issue']).to include('iteration' => hash_including('id' => current_iteration.to_global_id.to_s))
end
context 'when iterationId is provided' do
let(:input) do
{
'title' => 'new title',
'weight' => 2,
'healthStatus' => 'atRisk',
'iterationId' => current_iteration.to_global_id.to_s
}
end
it 'creates the issue' do
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response['issue']).to include(input.except('iterationId'))
expect(mutation_response['issue']).to include('iteration' => hash_including('id' => current_iteration.to_global_id.to_s))
end
context 'when iterationId and iterationWildcardId are provided' do
let(:input) do
{
'title' => 'new title',
'weight' => 2,
'healthStatus' => 'atRisk',
'iterationId' => current_iteration.to_global_id.to_s,
'iterationWildcardId' => 'CURRENT',
'iterationCadenceId' => current_iteration.iterations_cadence.to_global_id.to_s
}
end
it 'returns a mutually exclusive argument error' do
post_graphql_mutation(mutation, current_user: current_user)
expect(graphql_errors).to contain_exactly(hash_including('message' => 'Incompatible arguments: iterationId, iterationWildcardId.'))
end
end
end end
end end
...@@ -7,8 +7,11 @@ RSpec.describe Issues::CreateService do ...@@ -7,8 +7,11 @@ RSpec.describe Issues::CreateService do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be_with_reload(:project) { create(:project, group: group) } let_it_be_with_reload(:project) { create(:project, group: group) }
let(:params) { { title: 'Awesome issue', description: 'please fix', weight: 9 } } let(:base_params) { { title: 'Awesome issue', description: 'please fix', weight: 9 } }
let(:additional_params) { {} }
let(:params) { base_params.merge(additional_params) }
let(:service) { described_class.new(project: project, current_user: user, params: params, spam_params: nil) } let(:service) { described_class.new(project: project, current_user: user, params: params, spam_params: nil) }
let(:created_issue) { service.execute }
describe '#execute' do describe '#execute' do
context 'when current user cannot admin issues in the project' do context 'when current user cannot admin issues in the project' do
...@@ -17,10 +20,8 @@ RSpec.describe Issues::CreateService do ...@@ -17,10 +20,8 @@ RSpec.describe Issues::CreateService do
end end
it 'filters out params that cannot be set without the :admin_issue permission' do it 'filters out params that cannot be set without the :admin_issue permission' do
issue = service.execute expect(created_issue).to be_persisted
expect(created_issue.weight).to be_nil
expect(issue).to be_persisted
expect(issue.weight).to be_nil
end end
end end
...@@ -31,10 +32,8 @@ RSpec.describe Issues::CreateService do ...@@ -31,10 +32,8 @@ RSpec.describe Issues::CreateService do
end end
it 'sets permitted params correctly' do it 'sets permitted params correctly' do
issue = service.execute expect(created_issue).to be_persisted
expect(created_issue.weight).to eq(9)
expect(issue).to be_persisted
expect(issue.weight).to eq(9)
end end
context 'when epics are enabled' do context 'when epics are enabled' do
...@@ -58,11 +57,9 @@ RSpec.describe Issues::CreateService do ...@@ -58,11 +57,9 @@ RSpec.describe Issues::CreateService do
let(:params) { { title: 'New issue', description: "/epic #{epic.to_reference(project)}" } } let(:params) { { title: 'New issue', description: "/epic #{epic.to_reference(project)}" } }
it 'adds an issue to the passed epic' do it 'adds an issue to the passed epic' do
issue = service.execute expect(created_issue).to be_persisted
expect(created_issue.reload.epic).to eq(epic)
expect(issue).to be_persisted expect(created_issue.confidential).to eq(false)
expect(issue.reload.epic).to eq(epic)
expect(issue.confidential).to eq(false)
end end
end end
...@@ -82,10 +79,8 @@ RSpec.describe Issues::CreateService do ...@@ -82,10 +79,8 @@ RSpec.describe Issues::CreateService do
end end
it 'sets epic and milestone to issuable and update epic start and due date' do it 'sets epic and milestone to issuable and update epic start and due date' do
issue = service.execute expect(created_issue.milestone).to eq(milestone)
expect(created_issue.reload.epic).to eq(epic)
expect(issue.milestone).to eq(milestone)
expect(issue.reload.epic).to eq(epic)
expect(epic.reload.start_date).to eq(milestone.start_date) expect(epic.reload.start_date).to eq(milestone.start_date)
expect(epic.due_date).to eq(milestone.due_date) expect(epic.due_date).to eq(milestone.due_date)
end end
...@@ -106,23 +101,82 @@ RSpec.describe Issues::CreateService do ...@@ -106,23 +101,82 @@ RSpec.describe Issues::CreateService do
end end
context 'when adding a public issue to confidential epic' do context 'when adding a public issue to confidential epic' do
it 'creates confidential child issue' do let(:confidential_epic) { create(:epic, group: group, confidential: true) }
confidential_epic = create(:epic, group: group, confidential: true) let(:params) { { title: 'confidential issue', epic_id: confidential_epic.id } }
params = { title: 'confidential issue', epic_id: confidential_epic.id }
issue = described_class.new(project: project, current_user: user, params: params, spam_params: nil).execute
expect(issue.confidential).to eq(true) it 'creates confidential child issue' do
expect(created_issue).to be_confidential
end end
end end
context 'when adding a confidential issue to public epic' do context 'when adding a confidential issue to public epic' do
let(:params) { { title: 'confidential issue', epic_id: epic.id, confidential: true } }
it 'creates a confidential child issue' do it 'creates a confidential child issue' do
params = { title: 'confidential issue', epic_id: epic.id, confidential: true } expect(created_issue).to be_confidential
end
end
end
end
context 'when iterations are available' do
let_it_be(:iteration_cadence1) { create(:iterations_cadence, group: group) }
let_it_be(:iteration_cadence2) { create(:iterations_cadence, group: group) }
let_it_be(:current_iteration1) { create(:iteration, group: group, iterations_cadence: iteration_cadence1, start_date: 4.days.ago, due_date: 3.days.from_now) }
let_it_be(:current_iteration2) { create(:iteration, group: group, iterations_cadence: iteration_cadence2, start_date: 4.days.ago, due_date: 3.days.from_now) }
let_it_be(:future_iteration) { create(:iteration, group: group, iterations_cadence: iteration_cadence1, start_date: 6.days.from_now, due_date: 13.days.from_now) }
before do
stub_licensed_features(iterations: true)
end
issue = described_class.new(project: project, current_user: user, params: params, spam_params: nil).execute context 'when iteration_id is provided' do
let(:additional_params) { { iteration_id: future_iteration.id } }
it 'is successful, and assigns the current iteration to the issue' do
expect(created_issue).to be_persisted
expect(created_issue).to have_attributes(iteration: future_iteration)
end
context 'when iteration_wildcard_id is provided' do
let(:additional_params) { { iteration_id: future_iteration.id, iteration_wildcard_id: 'CURRENT', iteration_cadence_id: iteration_cadence2.id } }
it 'raises a mutually exclusive argument error' do
expect { service.execute }.to raise_error(
::Issues::BaseService::IterationAssignmentError,
'Incompatible arguments: iteration_id, iteration_wildcard_id.'
)
end
end
context "when user can't read the given iteration" do
let(:additional_params) { { iteration_id: create(:iteration, group: create(:group, :private)).id } }
it 'is successful but does not assign the iteration' do
expect(created_issue).to be_persisted
expect(created_issue).to have_attributes(iteration: nil)
end
end
end
context 'when iteration_wildcard_id is CURRENT' do
let(:additional_params) { { iteration_wildcard_id: 'CURRENT' } }
context 'when iteration_cadence_id is provided' do
let(:additional_params) { { iteration_wildcard_id: 'CURRENT', iteration_cadence_id: iteration_cadence2.id } }
it 'is successful, and assigns the current iteration to the issue' do
expect(created_issue).to be_persisted
expect(created_issue).to have_attributes(iteration: current_iteration2)
end
end
expect(issue.confidential).to eq(true) context 'when iteration_cadence_id is not provided' do
it 'always requires iteration cadence id when wildcard is provided' do
expect { service.execute }.to raise_error(
::Issues::BaseService::IterationAssignmentError,
'iteration_cadence_id is required when iteration_wildcard_id is provided.'
)
end end
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment