Commit 8821ec56 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 c9555383
......@@ -71,7 +71,7 @@ module Mutations
def resolve(project_path:, **attributes)
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])
issue = ::Issues::CreateService.new(project: project, current_user: current_user, params: params, spam_params: spam_params).execute
......@@ -88,7 +88,8 @@ module Mutations
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[: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 }
......
......@@ -2,6 +2,8 @@
# Base class, scoped by project
class BaseProjectService < ::BaseContainerService
include ::Gitlab::Utils::StrongMemoize
attr_accessor :project
def initialize(project:, current_user: nil, params: {})
......@@ -11,4 +13,12 @@ class BaseProjectService < ::BaseContainerService
end
delegate :repository, to: :project
private
def project_group
strong_memoize(:project_group) do
project.group
end
end
end
......@@ -1271,6 +1271,9 @@ Input type: `CreateIssueInput`
| <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="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="mutationcreateissuelabels"></a>`labels` | [`[String!]`](#string) | Labels of the issue. |
| <a id="mutationcreateissuelocked"></a>`locked` | [`Boolean`](#boolean) | Indicates discussion is locked on the issue. |
......@@ -15820,6 +15823,14 @@ State of a GitLab issue or merge request.
| <a id="issuablestatelocked"></a>`locked` | Discussion has been locked. |
| <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`
Values for sorting issues.
......
......@@ -116,9 +116,7 @@ module EE
{
parent: params.parent,
include_ancestors: true,
state: 'opened',
start_date: Date.today,
end_date: Date.today
iteration_wildcard_id: ::Iteration::Predefined::Current.title
}
end
end
......
......@@ -23,6 +23,8 @@ class IterationsFinder
def execute(skip_authorization: false)
@skip_authorization = skip_authorization
handle_wildcard_params
items = Iteration.all
items = by_id(items)
items = by_iid(items)
......@@ -40,6 +42,14 @@ class IterationsFinder
attr_reader :skip_authorization
# wildcard params do not override other explicitly 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)
return Iteration.none unless skip_authorization || Ability.allowed?(current_user, :read_iteration, params[:parent])
......
......@@ -13,6 +13,17 @@ module EE
argument :epic_id, ::Types::GlobalIDType[::Epic],
required: false,
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
override :resolve
......@@ -20,18 +31,53 @@ module EE
super
rescue ActiveRecord::RecordNotFound => e
{ errors: [e.message], issue: nil }
rescue ::Issues::BaseService::IterationAssignmentError => e
raise(
::Gitlab::Graphql::Errors::ArgumentError,
transform_field_names(e.message)
)
end
private
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
# 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] = 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
......
# 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,8 @@ module EE
params.delete(:health_status)
end
[:iteration, :sprint_id].each { |iteration_param| params.delete(iteration_param) } unless can_admin_issuable
super
end
......
......@@ -5,7 +5,10 @@ module EE
module BaseService
extend ::Gitlab::Utils::Override
class EpicAssignmentError < ::ArgumentError; end
EpicAssignmentError = Class.new(::ArgumentError)
IterationAssignmentError = Class.new(StandardError)
ALLOWED_ITERATION_WILDCARDS = [::Iteration::Predefined::Current.title].freeze
def filter_epic(issue)
return unless epic_param_present?
......@@ -137,6 +140,51 @@ module EE
false
end
end
private
def validate_iteration_params!(iteration_params)
return false if [iteration_params[:iteration_wildcard_id], iteration_params[:iteration_id]].all?(&:blank?)
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
if iteration_params[:iteration_wildcard_id].present?
ALLOWED_ITERATION_WILDCARDS.any? { |wildcard| iteration_params[:iteration_wildcard_id].casecmp?(wildcard) }
else
true
end
end
def find_iteration!(iteration_params, group)
# 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
# These iteration params need to be removed unconditionally from params as they do not exist on the model
iteration_params = params.extract!(:iteration_wildcard_id, :iteration_cadence_id, :iteration_id)
return unless project_group&.licensed_feature_available?(:iterations)
return unless validate_iteration_params!(iteration_params)
iteration = find_iteration!(iteration_params, project_group)
params[:iteration] = iteration if iteration
end
end
end
end
......@@ -5,6 +5,13 @@ module EE
module CreateService
extend ::Gitlab::Utils::Override
override :create
def create(issuable, skip_system_notes: false)
process_iteration_id
super
end
override :filter_params
def filter_params(issue)
filter_epic(issue)
......
......@@ -67,6 +67,28 @@ RSpec.describe IterationsFinder do
it 'returns iterations for groups' do
expect(subject).to contain_exactly(closed_iteration, started_group_iteration, upcoming_group_iteration)
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
context 'iterations for project with ancestors' do
......@@ -134,6 +156,22 @@ RSpec.describe IterationsFinder do
expect(subject).to contain_exactly(closed_iteration, started_group_iteration, upcoming_group_iteration)
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
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)
......
......@@ -7,6 +7,8 @@ RSpec.describe Mutations::Issues::Create do
let_it_be(:group) { create(: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(:assignee1) { create(:user) }
let_it_be(:assignee2) { create(:user) }
......@@ -31,29 +33,119 @@ RSpec.describe Mutations::Issues::Create do
end
let(:mutation_params) do
inputs.merge(expected_attributes)
inputs.merge(expected_attributes).merge(additional_attributes)
end
let(:additional_attributes) { {} }
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) }
describe '#resolve' do
before do
project.add_guest(assignee1)
project.add_guest(assignee2)
stub_licensed_features(issuable_health_status: true)
stub_spam_services
end
subject { mutation.resolve(**mutation_params) }
subject(:resolved_mutation) { mutation.resolve(**mutation_params) }
context 'when user can create issues' do
before do
before_all do
group.add_developer(user)
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
expect(mutated_issue).to have_attributes(expected_attributes)
expect(mutated_issue.assignees.pluck(:id)).to eq([assignee1.id, assignee2.id])
......@@ -73,7 +165,7 @@ RSpec.describe Mutations::Issues::Create do
end
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)
end
......@@ -83,7 +175,7 @@ RSpec.describe Mutations::Issues::Create do
it 'is successful, but it does not add the epic' do
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)
end
end
......@@ -91,7 +183,7 @@ RSpec.describe Mutations::Issues::Create do
context 'epics are unavailable' 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
it 'does not create an issue' do
......
......@@ -6,13 +6,17 @@ RSpec.describe 'Create an issue' do
include GraphqlHelpers
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
{
'title' => 'new title',
'weight' => 2,
'healthStatus' => 'atRisk'
'healthStatus' => 'atRisk',
'iterationWildcardId' => 'CURRENT',
'iterationCadenceId' => current_iteration.iterations_cadence.to_global_id.to_s
}
end
......@@ -21,14 +25,53 @@ RSpec.describe 'Create an issue' do
let(:mutation_response) { graphql_mutation_response(:create_issue) }
before do
stub_licensed_features(issuable_health_status: true)
project.add_developer(current_user)
stub_licensed_features(issuable_health_status: true, iterations: true)
group.add_developer(current_user)
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)
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
......@@ -7,20 +7,27 @@ RSpec.describe Issues::CreateService do
let_it_be(:user) { create(:user) }
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(:created_issue) { service.execute }
describe '#execute' do
context 'when current user cannot admin issues in the project' do
let_it_be(:iteration) { create(:iteration, group: group, start_date: 14.days.ago, due_date: 5.days.ago) }
let(:additional_params) { { iteration: iteration, sprint_id: iteration.id } }
before do
project.add_guest(user)
stub_licensed_features(iterations: true)
end
it 'filters out params that cannot be set without the :admin_issue permission' do
issue = service.execute
expect(issue).to be_persisted
expect(issue.weight).to be_nil
expect(created_issue).to be_persisted
expect(created_issue.weight).to be_nil
expect(created_issue.iteration).to be_nil
end
end
......@@ -31,10 +38,8 @@ RSpec.describe Issues::CreateService do
end
it 'sets permitted params correctly' do
issue = service.execute
expect(issue).to be_persisted
expect(issue.weight).to eq(9)
expect(created_issue).to be_persisted
expect(created_issue.weight).to eq(9)
end
context 'when epics are enabled' do
......@@ -58,11 +63,9 @@ RSpec.describe Issues::CreateService do
let(:params) { { title: 'New issue', description: "/epic #{epic.to_reference(project)}" } }
it 'adds an issue to the passed epic' do
issue = service.execute
expect(issue).to be_persisted
expect(issue.reload.epic).to eq(epic)
expect(issue.confidential).to eq(false)
expect(created_issue).to be_persisted
expect(created_issue.reload.epic).to eq(epic)
expect(created_issue.confidential).to eq(false)
end
end
......@@ -82,10 +85,8 @@ RSpec.describe Issues::CreateService do
end
it 'sets epic and milestone to issuable and update epic start and due date' do
issue = service.execute
expect(issue.milestone).to eq(milestone)
expect(issue.reload.epic).to eq(epic)
expect(created_issue.milestone).to eq(milestone)
expect(created_issue.reload.epic).to eq(epic)
expect(epic.reload.start_date).to eq(milestone.start_date)
expect(epic.due_date).to eq(milestone.due_date)
end
......@@ -106,26 +107,112 @@ RSpec.describe Issues::CreateService do
end
context 'when adding a public issue to confidential epic' do
it 'creates confidential child issue' do
confidential_epic = create(:epic, group: group, confidential: true)
params = { title: 'confidential issue', epic_id: confidential_epic.id }
issue = described_class.new(project: project, current_user: user, params: params, spam_params: nil).execute
let(:confidential_epic) { create(:epic, group: group, confidential: true) }
let(:params) { { title: 'confidential issue', epic_id: confidential_epic.id } }
expect(issue.confidential).to eq(true)
it 'creates confidential child issue' do
expect(created_issue).to be_confidential
end
end
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
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
context 'when sprint_id is provided' do
let(:additional_params) { { sprint_id: future_iteration.id } }
it 'is successful, and assigns the specified iteration to the issue' do
expect(created_issue).to be_persisted
expect(created_issue).to have_attributes(iteration: future_iteration)
end
end
context 'when iteration_id is provided' do
let(:additional_params) { { iteration_id: future_iteration.id } }
issue = described_class.new(project: project, current_user: user, params: params, spam_params: nil).execute
it 'is successful, and assigns the specified 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 } }
expect(issue.confidential).to eq(true)
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 provided' do
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
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
context 'when iteration_wildcard_id is invalid' do
let(:additional_params) { { iteration_wildcard_id: 'INVALID', iteration_cadence_id: iteration_cadence2.id } }
it 'is successful, and does not assign an iteration to the issue' do
expect(created_issue).to be_persisted
expect(created_issue).to have_attributes(iteration: nil)
end
end
end
context 'when no iteration params are provided' do
it 'is successful, and does not assign an iteration to the issue' do
expect(created_issue).to be_persisted
expect(created_issue).to have_attributes(iteration: nil)
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