diff --git a/app/graphql/mutations/concerns/mutations/validate_time_estimate.rb b/app/graphql/mutations/concerns/mutations/validate_time_estimate.rb new file mode 100644 index 0000000000000000000000000000000000000000..82a56fd04f326f359106984a0b9dd1069ef2eea7 --- /dev/null +++ b/app/graphql/mutations/concerns/mutations/validate_time_estimate.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Mutations + module ValidateTimeEstimate + private + + def validate_time_estimate(time_estimate) + return unless time_estimate + + parsed_time_estimate = Gitlab::TimeTrackingFormatter.parse(time_estimate, keep_zero: true) + + if parsed_time_estimate.nil? + raise Gitlab::Graphql::Errors::ArgumentError, + 'timeEstimate must be formatted correctly, for example `1h 30m`' + elsif parsed_time_estimate < 0 + raise Gitlab::Graphql::Errors::ArgumentError, + 'timeEstimate must be greater than or equal to zero. ' \ + 'Remember that every new timeEstimate overwrites the previous value.' + end + end + end +end diff --git a/app/graphql/mutations/issues/update.rb b/app/graphql/mutations/issues/update.rb index 2a863893cf1a9d161d0db600fd0353cee1cef1b2..35deb9e0af83d2ee51aad6ae0f9b473e608a10b3 100644 --- a/app/graphql/mutations/issues/update.rb +++ b/app/graphql/mutations/issues/update.rb @@ -6,6 +6,7 @@ class Update < Base graphql_name 'UpdateIssue' include CommonMutationArguments + include ValidateTimeEstimate argument :title, GraphQL::Types::String, required: false, @@ -54,9 +55,7 @@ def ready?(label_ids: [], add_label_ids: [], remove_label_ids: [], time_estimate raise Gitlab::Graphql::Errors::ArgumentError, 'labelIds is mutually exclusive with any of addLabelIds or removeLabelIds' end - if !time_estimate.nil? && Gitlab::TimeTrackingFormatter.parse(time_estimate, keep_zero: true).nil? - raise Gitlab::Graphql::Errors::ArgumentError, 'timeEstimate must be formatted correctly, for example `1h 30m`' - end + validate_time_estimate(time_estimate) super end diff --git a/app/graphql/mutations/merge_requests/update.rb b/app/graphql/mutations/merge_requests/update.rb index da4db7342a39171f8b949302ce3deec054c485e2..470292df86c60f9dd987b7aa8c44d93be5a3a654 100644 --- a/app/graphql/mutations/merge_requests/update.rb +++ b/app/graphql/mutations/merge_requests/update.rb @@ -5,6 +5,8 @@ module MergeRequests class Update < Base graphql_name 'MergeRequestUpdate' + include ValidateTimeEstimate + description 'Update attributes of a merge request' argument :title, GraphQL::Types::String, @@ -45,10 +47,7 @@ def resolve(project_path:, iid:, **args) end def ready?(time_estimate: nil, **args) - if !time_estimate.nil? && Gitlab::TimeTrackingFormatter.parse(time_estimate, keep_zero: true).nil? - raise Gitlab::Graphql::Errors::ArgumentError, - 'timeEstimate must be formatted correctly, for example `1h 30m`' - end + validate_time_estimate(time_estimate) super end diff --git a/spec/graphql/mutations/issues/update_spec.rb b/spec/graphql/mutations/issues/update_spec.rb index ac82037b7e2b97eb82e73f1484bc3e50fda2105d..622ccb86b2ed66dcf64d2cc0099c459f4c233317 100644 --- a/spec/graphql/mutations/issues/update_spec.rb +++ b/spec/graphql/mutations/issues/update_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Mutations::Issues::Update do +RSpec.describe Mutations::Issues::Update, feature_category: :team_planning do let_it_be(:project) { create(:project) } let_it_be(:user) { create(:user) } let_it_be(:project_label) { create(:label, project: project) } @@ -177,6 +177,17 @@ end end + context 'when timeEstimate is negative' do + let(:time_estimate) { '-1h' } + + it 'raises an argument error and changes are not applied' do + expect { mutation.ready?(time_estimate: time_estimate) } + .to raise_error(Gitlab::Graphql::Errors::ArgumentError, + 'timeEstimate must be greater than or equal to zero. Remember that every new timeEstimate overwrites the previous value.') + expect { subject }.not_to change { issue.time_estimate } + end + end + context 'when timeEstimate is 0' do let(:time_estimate) { '0' } diff --git a/spec/graphql/mutations/merge_requests/update_spec.rb b/spec/graphql/mutations/merge_requests/update_spec.rb index 8a10f6cadd0e6a0d11ed40f2d43d4d28dcbdfcbd..6ced71c5f4c30e8f9fd316968e433a271c45e78b 100644 --- a/spec/graphql/mutations/merge_requests/update_spec.rb +++ b/spec/graphql/mutations/merge_requests/update_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Mutations::MergeRequests::Update do +RSpec.describe Mutations::MergeRequests::Update, feature_category: :team_planning do let(:merge_request) { create(:merge_request) } let(:user) { create(:user) } @@ -59,6 +59,18 @@ end end + context 'when timeEstimate is negative' do + let(:time_estimate) { '-1h' } + + it 'raises an argument error and changes are not applied' do + expect { mutation.ready?(time_estimate: time_estimate) } + .to raise_error(Gitlab::Graphql::Errors::ArgumentError, + 'timeEstimate must be greater than or equal to zero. ' \ + 'Remember that every new timeEstimate overwrites the previous value.') + expect { subject }.not_to change { merge_request.time_estimate } + end + end + context 'when timeEstimate is 0' do let(:time_estimate) { '0' } diff --git a/spec/lib/gitlab/time_tracking_formatter_spec.rb b/spec/lib/gitlab/time_tracking_formatter_spec.rb index 4203a76cbfbf35aa91a6770dc71a532d262fe159..aa755d64a7a24480d7d13a92bc2df8a4acab72b7 100644 --- a/spec/lib/gitlab/time_tracking_formatter_spec.rb +++ b/spec/lib/gitlab/time_tracking_formatter_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::TimeTrackingFormatter do +RSpec.describe Gitlab::TimeTrackingFormatter, feature_category: :team_planning do describe '#parse' do let(:keep_zero) { false } diff --git a/spec/requests/api/graphql/mutations/issues/update_spec.rb b/spec/requests/api/graphql/mutations/issues/update_spec.rb index 97ead687a82f82ccda5e085b754d39c4569a8477..ff100d99628711529fce4b8dcdbc2fcceb4f8b94 100644 --- a/spec/requests/api/graphql/mutations/issues/update_spec.rb +++ b/spec/requests/api/graphql/mutations/issues/update_spec.rb @@ -147,5 +147,10 @@ end end end + + it_behaves_like 'updating time estimate' do + let(:resource) { issue } + let(:mutation_name) { 'updateIssue' } + end end end diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_time_estimate_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_time_estimate_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..6bc130a97cf9fe66c8062320a84b6f625c7089e5 --- /dev/null +++ b/spec/requests/api/graphql/mutations/merge_requests/set_time_estimate_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Setting time estimate of a merge request', feature_category: :code_review_workflow do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + let_it_be(:project) { create(:project, :public) } + let_it_be(:merge_request) { create(:merge_request, source_project: project) } + + let(:input) do + { + iid: merge_request.iid.to_s + } + end + + let(:extra_params) { { project_path: project.full_path } } + let(:input_params) { input.merge(extra_params) } + let(:mutation) { graphql_mutation(:merge_request_update, input_params, nil, ['productAnalyticsState']) } + let(:mutation_response) { graphql_mutation_response(:merge_request_update) } + + context 'when the user is not allowed to update a merge request' do + before_all do + project.add_reporter(current_user) + end + + it_behaves_like 'a mutation that returns a top-level access error' + end + + context 'when updating a time estimate' do + before_all do + project.add_developer(current_user) + end + + it_behaves_like 'updating time estimate' do + let(:resource) { merge_request } + let(:mutation_name) { 'mergeRequestUpdate' } + end + end +end diff --git a/spec/support/shared_examples/graphql/mutations/update_time_estimate_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/update_time_estimate_shared_examples.rb new file mode 100644 index 0000000000000000000000000000000000000000..d6d360bb413a94eeedf3240e0bb41520a8af0448 --- /dev/null +++ b/spec/support/shared_examples/graphql/mutations/update_time_estimate_shared_examples.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'updating time estimate' do + context 'when setting time estimate', :aggregate_failures do + using RSpec::Parameterized::TableSyntax + + let(:input_params) { input.merge(extra_params).merge({ timeEstimate: time_estimate }) } + + context 'when time estimate is not a valid numerical value' do + let(:time_estimate) { '-3.5d' } + + it 'does not update' do + expect { post_graphql_mutation(mutation, current_user: current_user) }.not_to change { resource.time_estimate } + end + + it 'returns error' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(graphql_errors).to include(a_hash_including('message' => /must be greater than or equal to zero/)) + end + end + + context 'when time estimate is not a number' do + let(:time_estimate) { 'nonsense' } + + it 'does not update' do + expect { post_graphql_mutation(mutation, current_user: current_user) }.not_to change { resource.time_estimate } + end + + it 'returns error' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(graphql_errors).to include(a_hash_including('message' => /must be formatted correctly/)) + end + end + + context 'when time estimate is valid' do + let(:time_estimate) { "1h" } + + before do + post_graphql_mutation(mutation, current_user: current_user) + end + + it_behaves_like 'a working GraphQL mutation' + + where(:time_estimate, :value) do + '1h' | 3600 + '0h' | 0 + '-0h' | 0 + end + + with_them do + specify do + expect(graphql_data_at(mutation_name, resource.class.to_s.underscore, 'timeEstimate')).to eq(value) + end + end + end + end +end