diff --git a/.rubocop_todo/rspec/before_all_role_assignment.yml b/.rubocop_todo/rspec/before_all_role_assignment.yml index 20ec3996b0795dbb068aba55c5cb46063cf2df1b..0e66a529b74d0cec1e3e06778de4e9eb1d2b3e06 100644 --- a/.rubocop_todo/rspec/before_all_role_assignment.yml +++ b/.rubocop_todo/rspec/before_all_role_assignment.yml @@ -703,7 +703,6 @@ RSpec/BeforeAllRoleAssignment: - 'ee/spec/services/vulnerability_issue_links/bulk_create_service_spec.rb' - 'ee/spec/services/vulnerability_issue_links/delete_service_spec.rb' - 'ee/spec/services/vulnerability_merge_request_links/create_service_spec.rb' - - 'ee/spec/services/work_items/widgets/health_status_service/update_service_spec.rb' - 'ee/spec/services/work_items/widgets/iteration_service/update_service_spec.rb' - 'ee/spec/services/work_items/widgets/progress_service/update_service_spec.rb' - 'ee/spec/services/work_items/widgets/status_service/update_service_spec.rb' diff --git a/.rubocop_todo/rspec/named_subject.yml b/.rubocop_todo/rspec/named_subject.yml index 6cff49a0981a4c4292ca84cfec96a0d97c262622..eb4e7ab06bbef415dd032e925f1afeed3040783b 100644 --- a/.rubocop_todo/rspec/named_subject.yml +++ b/.rubocop_todo/rspec/named_subject.yml @@ -1140,7 +1140,6 @@ RSpec/NamedSubject: - 'ee/spec/services/vulnerability_external_issue_links/create_service_spec.rb' - 'ee/spec/services/vulnerability_feedback/create_service_spec.rb' - 'ee/spec/services/work_items/update_service_spec.rb' - - 'ee/spec/services/work_items/widgets/health_status_service/update_service_spec.rb' - 'ee/spec/services/work_items/widgets/iteration_service/update_service_spec.rb' - 'ee/spec/services/work_items/widgets/progress_service/update_service_spec.rb' - 'ee/spec/services/work_items/widgets/status_service/update_service_spec.rb' diff --git a/app/services/issuable/callbacks/base.rb b/app/services/issuable/callbacks/base.rb index 368dd76c16c0c143c59098e061f810cb0cd7fce6..5ac7ccf54fc3b1a119c254fd77b4753ea97a368b 100644 --- a/app/services/issuable/callbacks/base.rb +++ b/app/services/issuable/callbacks/base.rb @@ -5,7 +5,7 @@ module Callbacks class Base include Gitlab::Allowable - def initialize(issuable:, current_user:, params:) + def initialize(issuable:, current_user:, params: {}) @issuable = issuable @current_user = current_user @params = params diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 5e706605e0af7fdfae351056380b7f2f07cf104f..fbae22b968136747f92d2284cce2368a614fb4ff 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -9442,9 +9442,11 @@ Input type: `WorkItemCreateInput` | Name | Type | Description | | ---- | ---- | ----------- | | <a id="mutationworkitemcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| <a id="mutationworkitemcreatecolorwidget"></a>`colorWidget` | [`WorkItemWidgetColorInput`](#workitemwidgetcolorinput) | Input for color widget. | | <a id="mutationworkitemcreateconfidential"></a>`confidential` | [`Boolean`](#boolean) | Sets the work item confidentiality. | | <a id="mutationworkitemcreatedescription"></a>`description` **{warning-solid}** | [`String`](#string) | **Deprecated:** use description widget instead. Deprecated in GitLab 16.9. | | <a id="mutationworkitemcreatedescriptionwidget"></a>`descriptionWidget` | [`WorkItemWidgetDescriptionInput`](#workitemwidgetdescriptioninput) | Input for description widget. | +| <a id="mutationworkitemcreatehealthstatuswidget"></a>`healthStatusWidget` | [`WorkItemWidgetHealthStatusInput`](#workitemwidgethealthstatusinput) | Input for health status widget. | | <a id="mutationworkitemcreatehierarchywidget"></a>`hierarchyWidget` | [`WorkItemWidgetHierarchyCreateInput`](#workitemwidgethierarchycreateinput) | Input for hierarchy widget. | | <a id="mutationworkitemcreateiterationwidget"></a>`iterationWidget` | [`WorkItemWidgetIterationInput`](#workitemwidgetiterationinput) | Iteration widget of the work item. | | <a id="mutationworkitemcreatemilestonewidget"></a>`milestoneWidget` | [`WorkItemWidgetMilestoneInput`](#workitemwidgetmilestoneinput) | Input for milestone widget. | diff --git a/ee/app/graphql/ee/mutations/work_items/create.rb b/ee/app/graphql/ee/mutations/work_items/create.rb index 8c8816fab60feeff2f7e4bc09662eb5a7d8ec15e..70dd6ec333eac7a0dddeab4c4c4a63e9c7bf7a28 100644 --- a/ee/app/graphql/ee/mutations/work_items/create.rb +++ b/ee/app/graphql/ee/mutations/work_items/create.rb @@ -7,6 +7,11 @@ module Create extend ActiveSupport::Concern prepended do + argument :health_status_widget, + ::Types::WorkItems::Widgets::HealthStatusInputType, + required: false, + description: 'Input for health status widget.' + argument :iteration_widget, ::Types::WorkItems::Widgets::IterationInputType, required: false, @@ -17,6 +22,10 @@ module Create required: false, description: 'Input for rolledup dates widget.', alpha: { milestone: '16.9' } + + argument :color_widget, ::Types::WorkItems::Widgets::ColorInputType, + required: false, + description: 'Input for color widget.' end end end diff --git a/ee/app/services/work_items/callbacks/color.rb b/ee/app/services/work_items/callbacks/color.rb index aac5fd2bb5d7c4e0e8bee6e61c8ea4c4ff0ce1e7..59f063ff6450fb9d72e1790a2ecae967f9006fad 100644 --- a/ee/app/services/work_items/callbacks/color.rb +++ b/ee/app/services/work_items/callbacks/color.rb @@ -2,7 +2,7 @@ module WorkItems module Callbacks - class Color < ::WorkItems::Callbacks::Base + class Color < Base ALLOWED_PARAMS = %i[color skip_system_notes].freeze def after_initialize diff --git a/ee/app/services/work_items/callbacks/health_status.rb b/ee/app/services/work_items/callbacks/health_status.rb new file mode 100644 index 0000000000000000000000000000000000000000..0ff7c4d09a0eb6c8e434ee8be57374e72a02f632 --- /dev/null +++ b/ee/app/services/work_items/callbacks/health_status.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module WorkItems + module Callbacks + class HealthStatus < Base + def after_initialize + params[:health_status] = nil if excluded_in_new_type? + return unless params.key?(:health_status) && can_set_health_status? + + work_item.health_status = params[:health_status] + end + + private + + def can_set_health_status? + work_item.resource_parent&.feature_available?(:issuable_health_status) && has_permission?(:admin_work_item) + end + end + end +end diff --git a/ee/app/services/work_items/widgets/health_status_service/update_service.rb b/ee/app/services/work_items/widgets/health_status_service/update_service.rb deleted file mode 100644 index c307ca085d10dee9d6a8be48f9da3d9c26fe5fd2..0000000000000000000000000000000000000000 --- a/ee/app/services/work_items/widgets/health_status_service/update_service.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -module WorkItems - module Widgets - module HealthStatusService - class UpdateService < WorkItems::Widgets::BaseService - def before_update_callback(params:) - params[:health_status] = nil if new_type_excludes_widget? - - return unless params&.key?(:health_status) - return unless health_status_available? && has_permission?(:admin_work_item) - - work_item.health_status = params[:health_status] - end - - def health_status_available? - work_item.resource_parent&.feature_available?(:issuable_health_status) - end - end - end - end -end diff --git a/ee/spec/requests/api/graphql/mutations/work_items/create_spec.rb b/ee/spec/requests/api/graphql/mutations/work_items/create_spec.rb index a31f6a8c50d6be9a4d8a6b8bda75bd8cae76f329..f5787e726c10a7f50546afb82e5eb9df4026f279 100644 --- a/ee/spec/requests/api/graphql/mutations/work_items/create_spec.rb +++ b/ee/spec/requests/api/graphql/mutations/work_items/create_spec.rb @@ -12,71 +12,17 @@ let(:mutation_response) { graphql_mutation_response(:work_item_create) } let(:widgets_response) { mutation_response['workItem']['widgets'] } - RSpec.shared_examples 'creates work item' do - context 'when setting iteration on work item creation' do - let_it_be(:cadence) { create(:iterations_cadence, group: group) } - let_it_be(:iteration) { create(:iteration, iterations_cadence: cadence) } - - let(:input) do - { - title: 'new title', - workItemTypeId: WorkItems::Type.default_by_type(:task).to_global_id.to_s, - iterationWidget: { 'iterationId' => iteration.to_global_id.to_s } - } - end - - before do - stub_licensed_features(iterations: true) - end - - it "sets the work item's iteration", :aggregate_failures do - expect do - post_graphql_mutation(mutation, current_user: current_user) - end.to change { WorkItem.count }.by(1) - - expect(response).to have_gitlab_http_status(:success) - expect(widgets_response).to include( - { - 'type' => 'ITERATION', - 'iteration' => { 'id' => iteration.to_global_id.to_s } - } - ) - end - - context 'when iterations feature is unavailable' do - before do - stub_licensed_features(iterations: false) - end - - # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/383322 - # We prefer to return an error rather than nil when authorization for an object fails. - # Here the authorization fails due to the unavailability of the licensed feature. - # Because the object to be authorized gets loaded via argument inside an InputObject, - # we need to add an additional hook to Types::BaseInputObject so errors are raised. - it 'returns nil' do - expect do - post_graphql_mutation(mutation, current_user: current_user) - end.to change { WorkItem.count }.by(0) - - expect(mutation_response).to be_nil - end - end - end - - context 'when creating a key result' do - let_it_be(:parent) { create(:work_item, :objective, **container_params) } + context 'when user has permissions to create a work item' do + let(:current_user) { developer } + shared_examples 'creates work item with iteration widget' do let(:fields) do <<~FIELDS workItem { - id - workItemType { - id - } widgets { type - ... on WorkItemWidgetHierarchy { - parent { + ... on WorkItemWidgetIteration { + iteration { id } } @@ -86,22 +32,23 @@ FIELDS end - let(:input) do - { - title: 'key result', - workItemTypeId: WorkItems::Type.default_by_type(:key_result).to_global_id.to_s, - hierarchyWidget: { 'parentId' => parent.to_global_id.to_s } - } - end + context 'when setting iteration on work item creation' do + let_it_be(:cadence) { create(:iterations_cadence, group: group) } + let_it_be(:iteration) { create(:iteration, iterations_cadence: cadence) } - let(:widgets_response) { mutation_response['workItem']['widgets'] } + let(:input) do + { + title: 'new title', + workItemTypeId: WorkItems::Type.default_by_type(:task).to_global_id.to_s, + iterationWidget: { 'iterationId' => iteration.to_global_id.to_s } + } + end - context 'when okrs are available' do before do - stub_licensed_features(okrs: true) + stub_licensed_features(iterations: true) end - it 'creates the work item' do + it "sets the work item's iteration", :aggregate_failures do expect do post_graphql_mutation(mutation, current_user: current_user) end.to change { WorkItem.count }.by(1) @@ -109,206 +56,380 @@ expect(response).to have_gitlab_http_status(:success) expect(widgets_response).to include( { - 'parent' => { 'id' => parent.to_global_id.to_s }, - 'type' => 'HIERARCHY' + 'type' => 'ITERATION', + 'iteration' => { 'id' => iteration.to_global_id.to_s } } ) end + + context 'when iterations feature is unavailable' do + before do + stub_licensed_features(iterations: false) + end + + # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/383322 + # We prefer to return an error rather than nil when authorization for an object fails. + # Here the authorization fails due to the unavailability of the licensed feature. + # Because the object to be authorized gets loaded via argument inside an InputObject, + # we need to add an additional hook to Types::BaseInputObject so errors are raised. + it 'returns nil' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + end.to change { WorkItem.count }.by(0) + + expect(mutation_response).to be_nil + end + end + end + + context 'when creating a key result' do + let_it_be(:parent) { create(:work_item, :objective, **container_params) } + + let(:fields) do + <<~FIELDS + workItem { + id + workItemType { + id + } + widgets { + type + ... on WorkItemWidgetHierarchy { + parent { + id + } + } + } + } + errors + FIELDS + end + + let(:input) do + { + title: 'key result', + workItemTypeId: WorkItems::Type.default_by_type(:key_result).to_global_id.to_s, + hierarchyWidget: { 'parentId' => parent.to_global_id.to_s } + } + end + + let(:widgets_response) { mutation_response['workItem']['widgets'] } + + context 'when okrs are available' do + before do + stub_licensed_features(okrs: true) + end + + it 'creates the work item' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + end.to change { WorkItem.count }.by(1) + + expect(response).to have_gitlab_http_status(:success) + expect(widgets_response).to include( + { + 'parent' => { 'id' => parent.to_global_id.to_s }, + 'type' => 'HIERARCHY' + } + ) + end + end + + context 'when okrs are not available' do + before do + stub_licensed_features(okrs: false) + end + + it 'returns error' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + end.to not_change(WorkItem, :count) + + expect(mutation_response['errors']) + .to contain_exactly(/cannot be added: is not allowed to add this type of parent/) + expect(mutation_response['workItem']).to be_nil + end + end end - context 'when okrs are not available' do + context 'when group_webhooks feature is available', :aggregate_failures do + let(:input) do + { + title: 'new title', + workItemTypeId: WorkItems::Type.default_by_type(:task).to_global_id.to_s + } + end + before do - stub_licensed_features(okrs: false) + stub_licensed_features(group_webhooks: true) + create(:group_hook, issues_events: true, group: group) end - it 'returns error' do + it 'creates a work item' do expect do post_graphql_mutation(mutation, current_user: current_user) - end.to not_change(WorkItem, :count) + end.to change { WorkItem.count }.by(1) - expect(mutation_response['errors']) - .to contain_exactly(/cannot be added: is not allowed to add this type of parent/) - expect(mutation_response['workItem']).to be_nil + expect(response).to have_gitlab_http_status(:success) end end end - context 'when group_webhooks feature is available', :aggregate_failures do - let(:input) do - { - title: 'new title', - workItemTypeId: WorkItems::Type.default_by_type(:task).to_global_id.to_s - } - end + context 'when creating work items in a project' do + context 'with projectPath' do + let_it_be(:container_params) { { project: project } } + let(:mutation) { graphql_mutation(:workItemCreate, input.merge(projectPath: project.full_path), fields) } + let(:work_item_type) { :task } - before do - stub_licensed_features(group_webhooks: true) - create(:group_hook, issues_events: true, group: group) + it_behaves_like 'creates work item with iteration widget' end - it 'creates a work item' do - expect do - post_graphql_mutation(mutation, current_user: current_user) - end.to change { WorkItem.count }.by(1) + context 'with namespacePath' do + let_it_be(:container_params) { { project: project } } + let(:mutation) { graphql_mutation(:workItemCreate, input.merge(namespacePath: project.full_path), fields) } + let(:work_item_type) { :task } - expect(response).to have_gitlab_http_status(:success) + it_behaves_like 'creates work item with iteration widget' end end - end - context 'when user has permissions to create a work item' do - let(:current_user) { developer } + context 'when creating work items in a group' do + let_it_be(:container_params) { { namespace: group } } + let(:mutation) { graphql_mutation(:workItemCreate, input.merge(namespacePath: group.full_path), fields) } + let(:work_item_type) { :epic } - context 'with iteration widget input' do - let(:fields) do - <<~FIELDS + it_behaves_like 'creates work item with iteration widget' + + context 'with rolledup dates widget input' do + before do + stub_licensed_features(epics: true) + end + + let(:fields) do + <<~FIELDS workItem { widgets { type - ... on WorkItemWidgetIteration { - iteration { - id + ... on WorkItemWidgetRolledupDates { + startDate + startDateFixed + startDateIsFixed + startDateSourcingWorkItem { + id + } + startDateSourcingMilestone { + id + } + dueDate + dueDateFixed + dueDateIsFixed + startDateSourcingWorkItem { + id + } + dueDateSourcingMilestone { + id + } } - } } } errors - FIELDS - end + FIELDS + end - context 'when creating work items in a project' do - context 'with projectPath' do - let_it_be(:container_params) { { project: project } } - let(:mutation) { graphql_mutation(:workItemCreate, input.merge(projectPath: project.full_path), fields) } + context "when the work_items_rolledup_dates feature flag is disabled" do + before do + stub_feature_flags(work_items_rolledup_dates: false) + end - it_behaves_like 'creates work item' - end + let(:start_date) { 5.days.ago.to_date } + let(:due_date) { 5.days.from_now.to_date } - context 'with namespacePath' do - let_it_be(:container_params) { { project: project } } - let(:mutation) { graphql_mutation(:workItemCreate, input.merge(namespacePath: project.full_path), fields) } + let(:input) do + { + title: "some WI", + workItemTypeId: WorkItems::Type.default_by_type(:epic).to_gid.to_s, + rolledupDatesWidget: { + startDateFixed: start_date.to_s, + dueDateFixed: due_date.to_s + } + } + end - it_behaves_like 'creates work item' + it "does not set the work item's start and due date" do + expect { post_graphql_mutation(mutation, current_user: current_user) } + .to change { WorkItem.count }.by(1) + + expect(response).to have_gitlab_http_status(:success) + expect(widgets_response).to include( + "type" => "ROLLEDUP_DATES", + "dueDate" => nil, + "dueDateFixed" => nil, + "dueDateIsFixed" => nil, + "dueDateSourcingMilestone" => nil, + "startDate" => nil, + "startDateFixed" => nil, + "startDateIsFixed" => nil, + "startDateSourcingMilestone" => nil, + "startDateSourcingWorkItem" => nil + ) + end end - end - context 'when creating work items in a group' do - let_it_be(:container_params) { { namespace: group } } - let(:mutation) { graphql_mutation(:workItemCreate, input.merge(namespacePath: group.full_path), fields) } + context "with fixed dates" do + let(:start_date) { 5.days.ago.to_date } + let(:due_date) { 5.days.from_now.to_date } - it_behaves_like 'creates work item' + let(:input) do + { + title: "some WI", + workItemTypeId: WorkItems::Type.default_by_type(:epic).to_gid.to_s, + rolledupDatesWidget: { + startDateIsFixed: true, + startDateFixed: start_date.to_s, + dueDateIsFixed: true, + dueDateFixed: due_date.to_s + } + } + end - context "with rolledup dates widget input" do - before do - stub_licensed_features(epics: true) + it "sets the work item's start and due date" do + expect { post_graphql_mutation(mutation, current_user: current_user) } + .to change { WorkItem.count } + .by(1) + + expect(response).to have_gitlab_http_status(:success) + expect(widgets_response).to include( + "type" => "ROLLEDUP_DATES", + "dueDate" => due_date.to_s, + "dueDateFixed" => due_date.to_s, + "dueDateIsFixed" => true, + "dueDateSourcingMilestone" => nil, + "startDate" => start_date.to_s, + "startDateFixed" => start_date.to_s, + "startDateIsFixed" => true, + "startDateSourcingMilestone" => nil, + "startDateSourcingWorkItem" => nil + ) end + end + end + + context 'with health status widget input' do + let(:new_status) { 'onTrack' } + let(:input) do + { + title: "some WI", + workItemTypeId: WorkItems::Type.default_by_type(:epic).to_gid.to_s, + healthStatusWidget: { healthStatus: new_status } + } + end - let(:fields) do - <<~FIELDS + let(:fields) do + <<~FIELDS workItem { widgets { type - ... on WorkItemWidgetRolledupDates { - startDate - startDateFixed - startDateIsFixed - startDateSourcingWorkItem { - id - } - startDateSourcingMilestone { - id - } - dueDate - dueDateFixed - dueDateIsFixed - startDateSourcingWorkItem { - id - } - dueDateSourcingMilestone { - id - } - } + ... on WorkItemWidgetHealthStatus { + healthStatus + } } } errors - FIELDS - end + FIELDS + end - context "when the work_items_rolledup_dates feature flag is disabled" do - before do - stub_feature_flags(work_items_rolledup_dates: false) - end + context 'when issuable_health_status is licensed' do + before do + stub_licensed_features(epics: true, issuable_health_status: true) + end - let(:start_date) { 5.days.ago.to_date } - let(:due_date) { 5.days.from_now.to_date } + it 'sets value for the health status widget' do + expect { post_graphql_mutation(mutation, current_user: current_user) }.to change { WorkItem.count }.by(1) - let(:input) do + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['workItem']['widgets']).to include( { - title: "some WI", - workItemTypeId: WorkItems::Type.default_by_type(:epic).to_gid.to_s, - rolledupDatesWidget: { - startDateFixed: start_date.to_s, - dueDateFixed: due_date.to_s + 'healthStatus' => 'onTrack', + 'type' => 'HEALTH_STATUS' + } + ) + end + end + + context 'when issuable_health_status is unlicensed' do + before do + stub_licensed_features(epics: true, issuable_health_status: false) + end + + it 'returns an error' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + end.to change { WorkItem.count }.by(0) + + expect(mutation_response).to be_nil + expect(graphql_errors).to include(a_hash_including( + 'message' => "Following widget keys are not supported by Epic type: [:health_status_widget]" + )) + end + end + end + + context 'with color widget input' do + let(:new_color) { '#346465' } + let(:input) do + { + title: "some WI", + workItemTypeId: WorkItems::Type.default_by_type(:epic).to_gid.to_s, + colorWidget: { color: new_color } + } + end + + let(:fields) do + <<~FIELDS + workItem { + widgets { + type + ... on WorkItemWidgetColor { + color } } - end - - it "does not set the work item's start and due date" do - expect { post_graphql_mutation(mutation, current_user: current_user) } - .to change { WorkItem.count }.by(1) - - expect(response).to have_gitlab_http_status(:success) - expect(widgets_response).to include( - "type" => "ROLLEDUP_DATES", - "dueDate" => nil, - "dueDateFixed" => nil, - "dueDateIsFixed" => nil, - "dueDateSourcingMilestone" => nil, - "startDate" => nil, - "startDateFixed" => nil, - "startDateIsFixed" => nil, - "startDateSourcingMilestone" => nil, - "startDateSourcingWorkItem" => nil - ) - end + } + errors + FIELDS + end + + context 'when epic_colors is licensed' do + before do + stub_licensed_features(epics: true, epic_colors: true) end - context "with fixed dates" do - let(:start_date) { 5.days.ago.to_date } - let(:due_date) { 5.days.from_now.to_date } + it 'sets value for color widget' do + expect { post_graphql_mutation(mutation, current_user: current_user) }.to change { WorkItem.count }.by(1) - let(:input) do + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['workItem']['widgets']).to include( { - title: "some WI", - workItemTypeId: WorkItems::Type.default_by_type(:epic).to_gid.to_s, - rolledupDatesWidget: { - startDateIsFixed: true, - startDateFixed: start_date.to_s, - dueDateIsFixed: true, - dueDateFixed: due_date.to_s - } + 'color' => new_color, + 'type' => 'COLOR' } - end - - it "sets the work item's start and due date" do - expect { post_graphql_mutation(mutation, current_user: current_user) } - .to change { WorkItem.count } - .by(1) - - expect(response).to have_gitlab_http_status(:success) - expect(widgets_response).to include( - "type" => "ROLLEDUP_DATES", - "dueDate" => due_date.to_s, - "dueDateFixed" => due_date.to_s, - "dueDateIsFixed" => true, - "dueDateSourcingMilestone" => nil, - "startDate" => start_date.to_s, - "startDateFixed" => start_date.to_s, - "startDateIsFixed" => true, - "startDateSourcingMilestone" => nil, - "startDateSourcingWorkItem" => nil - ) - end + ) + end + end + + context 'when epic_colors is unlicensed' do + before do + stub_licensed_features(epics: true, epic_colors: false) + end + + it 'returns an error' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + end.to change { WorkItem.count }.by(0) + + expect(mutation_response).to be_nil + expect(graphql_errors).to include(a_hash_including( + 'message' => "Following widget keys are not supported by Epic type: [:color_widget]" + )) end end end diff --git a/ee/spec/services/work_items/callbacks/health_status_spec.rb b/ee/spec/services/work_items/callbacks/health_status_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..eb20479d2f17fa92c5363fe4ba80fd46b8a0ce2b --- /dev/null +++ b/ee/spec/services/work_items/callbacks/health_status_spec.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe WorkItems::Callbacks::HealthStatus, feature_category: :team_planning do + let_it_be(:user) { create(:user) } + let_it_be(:reporter) { create(:user) } + let_it_be(:group) { create(:group, reporters: reporter) } + let_it_be_with_reload(:work_item) do + create(:work_item, :epic, namespace: group, author: user, health_status: :on_track) + end + + let(:current_user) { reporter } + let(:params) { {} } + let(:callback) { described_class.new(issuable: work_item, current_user: current_user, params: params) } + + describe '#after_initialize' do + subject(:after_initialize_callback) { callback.after_initialize } + + shared_examples 'work item and health status is unchanged' do + it 'does not change work item health status value' do + expect { after_initialize_callback } + .to not_change { work_item.health_status } + .and not_change { work_item.updated_at } + end + end + + context 'when issuable_health_status feature is licensed' do + before do + stub_licensed_features(issuable_health_status: true) + end + + context 'when health_status param is present' do + context 'when health_status param is valid' do + let(:params) { { health_status: :needs_attention } } + + it 'updates work item health status value' do + expect { after_initialize_callback }.to change { work_item.health_status }.to('needs_attention') + end + end + + context 'when widget does not exist in new type' do + let(:params) { {} } + + before do + allow(callback).to receive(:excluded_in_new_type?).and_return(true) + end + + it "sets the work item's health status as nil" do + expect { callback.after_initialize }.to change { work_item.health_status }.from('on_track').to(nil) + end + end + end + + context 'when health_status param is not present' do + let(:params) { {} } + + it_behaves_like 'work item and health status is unchanged' + end + + context 'when param value is the same as the work item health status' do + let(:params) { { health_status: :on_track } } + + it_behaves_like 'work item and health status is unchanged' + end + + context 'when user cannot admin_work_item' do + let(:current_user) { user } + let(:params) { { health_status: :needs_attention } } + + it_behaves_like 'work item and health status is unchanged' + end + end + + context 'when issuable_health_status feature is unlicensed' do + before do + stub_licensed_features(issuable_health_status: false) + end + + it_behaves_like 'work item and health status is unchanged' + end + end +end diff --git a/ee/spec/services/work_items/widgets/health_status_service/update_service_spec.rb b/ee/spec/services/work_items/widgets/health_status_service/update_service_spec.rb deleted file mode 100644 index 5bb929c7d299085effb0fab482f7e089ca22d2fd..0000000000000000000000000000000000000000 --- a/ee/spec/services/work_items/widgets/health_status_service/update_service_spec.rb +++ /dev/null @@ -1,71 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe WorkItems::Widgets::HealthStatusService::UpdateService, feature_category: :team_planning do - let_it_be(:user) { create(:user) } - let_it_be(:project) { create(:project) } - let_it_be_with_reload(:work_item) { create(:work_item, project: project, author: user, health_status: nil) } - - let(:widget) { work_item.widgets.find { |widget| widget.is_a?(WorkItems::Widgets::HealthStatus) } } - let(:new_health_status) { :on_track } - let(:params) { { health_status: new_health_status } } - - describe '#update' do - let(:service) { described_class.new(widget: widget, current_user: user) } - - subject(:update_health_status) { service.before_update_callback(params: params) } - - before do - stub_licensed_features(issuable_health_status: true) - end - - shared_examples 'health_status is unchanged' do - it 'does not change the health_status of the work item' do - expect { update_health_status } - .to not_change { work_item.health_status } - end - end - - context 'when it has issuable_health_status license' do - context 'when health_status param is not present' do - let(:params) { {} } - - it_behaves_like 'health_status is unchanged' - end - - context 'when user can not admin work item' do - before do - project.add_guest(user) - end - - it_behaves_like 'health_status is unchanged' - end - - context 'when user can admin the work item' do - before do - project.add_reporter(user) - end - - it 'sets the health_status for the work item and triggers subscription' do - update_health_status - - expect(work_item.health_status).to eq('on_track') - end - - context 'when widget does not exist in new type' do - let(:params) { {} } - - before do - allow(service).to receive(:new_type_excludes_widget?).and_return(true) - work_item.health_status = 'on_track' - end - - it "resets the work item's health status" do - expect { subject }.to change { work_item.health_status }.from('on_track').to(nil) - end - end - end - end - end -end