Commit a09996b3 authored by Steve Abrams's avatar Steve Abrams

Merge branch '292268-destroy-iteration-mutation' into 'master'

Add destroyIteration mutation

See merge request gitlab-org/gitlab!60655
parents 2c728a0a e24c5aca
...@@ -2563,6 +2563,25 @@ Input type: `IterationCadenceUpdateInput` ...@@ -2563,6 +2563,25 @@ Input type: `IterationCadenceUpdateInput`
| <a id="mutationiterationcadenceupdateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | <a id="mutationiterationcadenceupdateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationiterationcadenceupdateiterationcadence"></a>`iterationCadence` | [`IterationCadence`](#iterationcadence) | The updated iteration cadence. | | <a id="mutationiterationcadenceupdateiterationcadence"></a>`iterationCadence` | [`IterationCadence`](#iterationcadence) | The updated iteration cadence. |
### `Mutation.iterationDelete`
Input type: `IterationDeleteInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationiterationdeleteclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationiterationdeleteid"></a>`id` | [`IterationID!`](#iterationid) | ID of the iteration. |
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationiterationdeleteclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationiterationdeleteerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationiterationdeletegroup"></a>`group` | [`Group!`](#group) | Group the iteration belongs to. |
### `Mutation.jiraImportStart` ### `Mutation.jiraImportStart`
Input type: `JiraImportStartInput` Input type: `JiraImportStartInput`
......
...@@ -25,6 +25,7 @@ module EE ...@@ -25,6 +25,7 @@ module EE
mount_mutation ::Mutations::GitlabSubscriptions::Activate mount_mutation ::Mutations::GitlabSubscriptions::Activate
mount_mutation ::Mutations::Iterations::Create mount_mutation ::Mutations::Iterations::Create
mount_mutation ::Mutations::Iterations::Update mount_mutation ::Mutations::Iterations::Update
mount_mutation ::Mutations::Iterations::Delete
mount_mutation ::Mutations::Iterations::Cadences::Create mount_mutation ::Mutations::Iterations::Cadences::Create
mount_mutation ::Mutations::Iterations::Cadences::Update mount_mutation ::Mutations::Iterations::Cadences::Update
mount_mutation ::Mutations::Iterations::Cadences::Destroy mount_mutation ::Mutations::Iterations::Cadences::Destroy
......
# frozen_string_literal: true
module Mutations
module Iterations
class Delete < BaseMutation
graphql_name 'IterationDelete'
authorize :admin_iteration
argument :id, ::Types::GlobalIDType[::Iteration], required: true,
description: copy_field_description(Types::IterationType, :id)
field :group, ::Types::GroupType, null: false, description: 'Group the iteration belongs to.'
def resolve(id:)
iteration = authorized_find!(id: id)
response = ::Iterations::DeleteService.new(iteration, current_user).execute
{
group: response.payload[:group],
errors: response.errors
}
end
private
def find_object(id:)
# TODO: Remove coercion when working on https://gitlab.com/gitlab-org/gitlab/-/issues/257883
id = ::Types::GlobalIDType[::Iteration].coerce_isolated_input(id)
GitlabSchema.find_by_gid(id)
end
end
end
end
...@@ -47,11 +47,13 @@ module EE ...@@ -47,11 +47,13 @@ module EE
before_validation :set_iterations_cadence, unless: -> { project_id.present? } before_validation :set_iterations_cadence, unless: -> { project_id.present? }
before_create :set_past_iteration_state before_create :set_past_iteration_state
before_destroy :check_if_can_be_destroyed
scope :upcoming, -> { with_state(:upcoming) } scope :upcoming, -> { with_state(:upcoming) }
scope :started, -> { with_state(:started) } scope :started, -> { with_state(:started) }
scope :closed, -> { with_state(:closed) } scope :closed, -> { with_state(:closed) }
scope :by_iteration_cadence_ids, ->(cadence_ids) { where(iterations_cadence_id: cadence_ids) } scope :by_iteration_cadence_ids, ->(cadence_ids) { where(iterations_cadence_id: cadence_ids) }
scope :with_start_date_after, ->(date) { where('start_date > :date', date: date) }
scope :within_timeframe, -> (start_date, end_date) do scope :within_timeframe, -> (start_date, end_date) do
where('start_date <= ?', end_date).where('due_date >= ?', start_date) where('start_date <= ?', end_date).where('due_date >= ?', start_date)
...@@ -140,6 +142,19 @@ module EE ...@@ -140,6 +142,19 @@ module EE
private private
def last_iteration_in_cadence?
!::Iteration.by_iteration_cadence_ids(iterations_cadence_id).with_start_date_after(due_date).exists?
end
def check_if_can_be_destroyed
return if closed?
unless last_iteration_in_cadence?
errors.add(:base, "upcoming/current iterations can't be deleted unless they are the last one in the cadence")
throw :abort # rubocop: disable Cop/BanCatchThrow
end
end
def timebox_format_reference(format = :id) def timebox_format_reference(format = :id)
raise ::ArgumentError, _('Unknown format') unless [:id, :name].include?(format) raise ::ArgumentError, _('Unknown format') unless [:id, :name].include?(format)
......
# frozen_string_literal: true
module Iterations
class DeleteService
include Gitlab::Allowable
def initialize(iteration, user)
@iteration = iteration
@group = iteration.group
@current_user = user
end
def execute
response_payload = { group: @group }
return ::ServiceResponse.error(message: _('Operation not allowed'), payload: response_payload, http_status: 403) unless can_delete_iteration?
if delete_and_remove_references
::ServiceResponse.success(payload: response_payload)
else
::ServiceResponse.error(message: iteration.errors.full_messages, payload: response_payload, http_status: 422)
end
end
private
attr_reader :iteration, :current_user, :group
def can_delete_iteration?
group.licensed_feature_available?(:iterations) &&
can?(current_user, :admin_iteration, iteration)
end
def delete_and_remove_references
ApplicationRecord.transaction do
if Board.in_iterations(iteration).update_all(iteration_id: nil) && iteration.destroy
true
else
raise ActiveRecord::Rollback
end
end
end
end
end
---
title: Add support to destroy iterations in GraphQL
merge_request: 60655
author:
type: added
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Deleting an iteration' do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:iteration, refind: true) { create(:iteration, group: group) }
let(:params) do
{ id: iteration.to_global_id.to_s }
end
let(:mutation) do
graphql_mutation(:iteration_delete, params)
end
def mutation_response
graphql_mutation_response(:iteration_delete)
end
context 'when the user does not have permission' do
before do
stub_licensed_features(iterations: true)
end
it_behaves_like 'a mutation that returns a top-level access error'
end
context 'when the user has permission' do
before do
group.add_developer(current_user)
end
context 'when iterations feature is disabled' do
before do
stub_licensed_features(iterations: false)
end
it_behaves_like 'a mutation that returns top-level errors', errors: [
'The resource that you are attempting to access does not exist or you don\'t have permission to ' \
'perform this action'
]
end
context 'when iterations feature is enabled' do
before do
stub_licensed_features(iterations: true)
end
it 'deletes the iteration', :aggregate_failures do
expect do
post_graphql_mutation(mutation, current_user: current_user)
end.to change(Iteration, :count).by(-1)
expect(mutation_response).to include('group' => hash_including('id' => group.to_global_id.to_s))
end
context 'when required arguments are missing' do
let(:params) { {} }
it 'returns error about required argument' do
post_graphql_mutation(mutation, current_user: current_user)
expect_graphql_errors_to_include(/was provided invalid value for id \(Expected value to not be null\)/)
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Iterations::DeleteService do
subject(:results) { described_class.new(iteration_to_delete, user).execute }
let_it_be(:group, refind: true) { create(:group) }
let_it_be(:project) { create(:project, :repository, group: group) }
let_it_be(:user) { create(:user) }
let_it_be(:start_date) { 3.weeks.ago }
let_it_be(:iteration_cadence1) { create(:iterations_cadence, group: group, start_date: start_date, duration_in_weeks: 1, iterations_in_advance: 2) }
let_it_be(:iteration_cadence2) { create(:iterations_cadence, group: group, start_date: start_date, duration_in_weeks: 1, iterations_in_advance: 2) }
let_it_be(:past_iteration, refind: true) { create(:closed_iteration, iterations_cadence: iteration_cadence1, group: group, start_date: start_date, due_date: start_date + 13.days) }
let_it_be(:past_board, refind: true) { create(:board, iteration: past_iteration, group: group) }
let_it_be(:past_issue, refind: true) { create(:issue, namespace: group, iteration: past_iteration) }
let_it_be(:past_merge_request, refind: true) { create(:merge_request, source_project: project, iteration: past_iteration) }
let_it_be(:current_iteration, refind: true) { create(:started_iteration, iterations_cadence: iteration_cadence1, group: group, start_date: start_date + 14.days, due_date: start_date + 27.days) }
let_it_be(:future_iteration, refind: true) { create(:upcoming_iteration, iterations_cadence: iteration_cadence1, group: group, start_date: start_date + 28.days, due_date: start_date + 41.days) }
let_it_be(:last_future_iteration, refind: true) { create(:upcoming_iteration, iterations_cadence: iteration_cadence1, group: group, start_date: start_date + 42.days, due_date: start_date + 55.days) }
let_it_be(:last_future_board, refind: true) { create(:board, iteration: last_future_iteration, group: group) }
let_it_be(:last_future_issue, refind: true) { create(:issue, namespace: group, iteration: last_future_iteration) }
let_it_be(:last_future_merge_request, refind: true) { create(:merge_request, source_branch: 'another-feature', source_project: project, iteration: last_future_iteration) }
let_it_be(:other_cadence_iteration, refind: true) { create(:started_iteration, iterations_cadence: iteration_cadence2, group: group, start_date: start_date + 14.days, due_date: start_date + 27.days) }
let_it_be(:other_cadence_board, refind: true) { create(:board, iteration: other_cadence_iteration, group: group) }
let_it_be(:other_cadence_issue, refind: true) { create(:issue, namespace: group, iteration: other_cadence_iteration) }
let_it_be(:other_cadence_merge_request, refind: true) { create(:merge_request, source_branch: 'another-feature2', source_project: project, iteration: other_cadence_iteration) }
let(:iteration_to_delete) { past_iteration }
RSpec.shared_examples 'iteration delete fails with message' do |message:|
it { is_expected.to be_error }
it 'returns not allowed message' do
expect(results.message).to eq(message)
end
it 'returns the iteration group as part of the response' do
expect(results.payload[:group]).to eq(group)
end
end
RSpec.shared_examples 'successfully deletes an iteration' do
it { is_expected.to be_success }
it 'deletes the iteration and associated records' do
expect do
results
associated_board.reload
associated_issue.reload
associated_mr.reload
end.to change(Iteration, :count).by(-1).and(
change(List, :count).by(-1)
).and(
change(ResourceIterationEvent, :count).by(-1)
).and(
change(Iteration, :count).by(-1)
).and(
change(associated_board, :iteration_id).from(iteration_to_delete.id).to(nil)
).and(
change(associated_issue, :iteration).from(iteration_to_delete).to(nil)
).and(
change(associated_mr, :iteration).from(iteration_to_delete).to(nil)
)
end
it 'returns the iteration group as part of the response' do
expect(results.payload[:group]).to eq(group)
end
end
before(:all) do
create(:iteration_list, iteration: past_iteration)
create(:resource_iteration_event, iteration: past_iteration)
create(:iteration_list, iteration: last_future_iteration)
create(:resource_iteration_event, iteration: last_future_iteration)
create(:iteration_list, iteration: other_cadence_iteration)
create(:resource_iteration_event, iteration: other_cadence_iteration)
end
describe '#execute' do
context 'when iterations feature enabled' do
before do
stub_licensed_features(iterations: true)
end
context 'when user is authorized' do
before do
group.add_developer(user)
end
context 'when deleting a past iteration' do
let(:iteration_to_delete) { past_iteration }
let(:associated_mr) { past_merge_request }
let(:associated_issue) { past_issue }
let(:associated_board) { past_board }
it_behaves_like 'successfully deletes an iteration'
end
context 'when deleting the current iteration' do
let(:iteration_to_delete) { current_iteration }
it_behaves_like 'iteration delete fails with message', message: ["upcoming/current iterations can't be deleted unless they are the last one in the cadence"]
end
context 'when deleting a future iteration that is not the last one' do
let(:iteration_to_delete) { future_iteration }
it_behaves_like 'iteration delete fails with message', message: ["upcoming/current iterations can't be deleted unless they are the last one in the cadence"]
end
context 'when deleting the last future iteration' do
let(:iteration_to_delete) { last_future_iteration }
let(:associated_mr) { last_future_merge_request }
let(:associated_issue) { last_future_issue }
let(:associated_board) { last_future_board }
it_behaves_like 'successfully deletes an iteration'
end
context 'when deleting the current iteration in another cadence' do
let(:iteration_to_delete) { other_cadence_iteration }
let(:associated_mr) { other_cadence_merge_request }
let(:associated_issue) { other_cadence_issue }
let(:associated_board) { other_cadence_board }
it_behaves_like 'successfully deletes an iteration'
end
end
context 'when user is not authorized' do
it_behaves_like 'iteration delete fails with message', message: 'Operation not allowed'
end
end
context 'when iterations feature disabled' do
before do
stub_licensed_features(iterations: false)
end
context 'when user is authorized' do
before do
group.add_developer(user)
end
it_behaves_like 'iteration delete fails with message', message: 'Operation not allowed'
end
context 'when user is not authorized' do
it_behaves_like 'iteration delete fails with message', message: 'Operation not allowed'
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