diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-07-20 18:40:28 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-07-20 18:40:28 +0300 |
commit | b595cb0c1dec83de5bdee18284abe86614bed33b (patch) | |
tree | 8c3d4540f193c5ff98019352f554e921b3a41a72 /spec/services/work_items | |
parent | 2f9104a328fc8a4bddeaa4627b595166d24671d0 (diff) |
Add latest changes from gitlab-org/gitlab@15-2-stable-eev15.2.0-rc42
Diffstat (limited to 'spec/services/work_items')
12 files changed, 706 insertions, 25 deletions
diff --git a/spec/services/work_items/create_and_link_service_spec.rb b/spec/services/work_items/create_and_link_service_spec.rb index 93c029bdab1..81be15f9e2f 100644 --- a/spec/services/work_items/create_and_link_service_spec.rb +++ b/spec/services/work_items/create_and_link_service_spec.rb @@ -7,13 +7,16 @@ RSpec.describe WorkItems::CreateAndLinkService do let_it_be(:project) { create(:project, group: group) } let_it_be(:user) { create(:user) } let_it_be(:related_work_item) { create(:work_item, project: project) } + let_it_be(:invalid_parent) { create(:work_item, :task, project: project) } let(:spam_params) { double } let(:link_params) { {} } + let(:params) do { title: 'Awesome work item', - description: 'please fix' + description: 'please fix', + work_item_type_id: WorkItems::Type.default_by_type(:task).id } end @@ -40,32 +43,32 @@ RSpec.describe WorkItems::CreateAndLinkService do end context 'when link params are valid' do - let(:link_params) { { issuable_references: [related_work_item.to_reference] } } + let(:link_params) { { parent_work_item: related_work_item } } it 'creates a work item successfully with links' do expect do service_result end.to change(WorkItem, :count).by(1).and( - change(IssueLink, :count).by(1) + change(WorkItems::ParentLink, :count).by(1) ) end end - context 'when link params are invalid' do - let(:link_params) { { issuable_references: ['invalid reference'] } } + context 'when link creation fails' do + let(:link_params) { { parent_work_item: invalid_parent } } it { is_expected.to be_error } it 'does not create a link and does not rollback transaction' do expect do service_result - end.to not_change(IssueLink, :count).and( + end.to not_change(WorkItems::ParentLink, :count).and( change(WorkItem, :count).by(1) ) end it 'returns a link creation error message' do - expect(service_result.errors).to contain_exactly('No matching issue found. Make sure that you are adding a valid issue URL.') + expect(service_result.errors).to contain_exactly(/only Issue and Incident can be parent of Task./) end end end @@ -84,7 +87,7 @@ RSpec.describe WorkItems::CreateAndLinkService do expect do service_result end.to not_change(WorkItem, :count).and( - not_change(IssueLink, :count) + not_change(WorkItems::ParentLink, :count) ) end diff --git a/spec/services/work_items/create_from_task_service_spec.rb b/spec/services/work_items/create_from_task_service_spec.rb index b4db925f053..7d2dab228b1 100644 --- a/spec/services/work_items/create_from_task_service_spec.rb +++ b/spec/services/work_items/create_from_task_service_spec.rb @@ -32,7 +32,7 @@ RSpec.describe WorkItems::CreateFromTaskService do expect do service_result end.to not_change(WorkItem, :count).and( - not_change(IssueLink, :count) + not_change(WorkItems::ParentLink, :count) ) end end @@ -47,12 +47,14 @@ RSpec.describe WorkItems::CreateFromTaskService do context 'when work item params are valid' do it { is_expected.to be_success } - it 'creates a work item and links it to the original work item successfully' do + it 'creates a work item and creates parent link to the original work item' do expect do service_result end.to change(WorkItem, :count).by(1).and( - change(IssueLink, :count) + change(WorkItems::ParentLink, :count).by(1) ) + + expect(work_item_to_update.reload.work_item_children).not_to be_empty end it 'replaces the original issue markdown description with new work item reference' do @@ -73,7 +75,7 @@ RSpec.describe WorkItems::CreateFromTaskService do expect do service_result end.to not_change(WorkItem, :count).and( - not_change(IssueLink, :count) + not_change(WorkItems::ParentLink, :count) ) end diff --git a/spec/services/work_items/create_service_spec.rb b/spec/services/work_items/create_service_spec.rb index f495e967b26..4009c85bacd 100644 --- a/spec/services/work_items/create_service_spec.rb +++ b/spec/services/work_items/create_service_spec.rb @@ -6,9 +6,12 @@ RSpec.describe WorkItems::CreateService do include AfterNextHelpers let_it_be_with_reload(:project) { create(:project) } + let_it_be(:parent) { create(:work_item, project: project) } let_it_be(:guest) { create(:user) } + let_it_be(:reporter) { create(:user) } let_it_be(:user_with_no_access) { create(:user) } + let(:widget_params) { {} } let(:spam_params) { double } let(:current_user) { guest } let(:opts) do @@ -20,10 +23,21 @@ RSpec.describe WorkItems::CreateService do before_all do project.add_guest(guest) + project.add_reporter(reporter) end describe '#execute' do - subject(:service_result) { described_class.new(project: project, current_user: current_user, params: opts, spam_params: spam_params).execute } + let(:service) do + described_class.new( + project: project, + current_user: current_user, + params: opts, + spam_params: spam_params, + widget_params: widget_params + ) + end + + subject(:service_result) { service.execute } before do stub_spam_services @@ -61,6 +75,14 @@ RSpec.describe WorkItems::CreateService do it 'returns validation errors' do expect(service_result.errors).to contain_exactly("Title can't be blank") end + + it 'does not execute after-create transaction widgets' do + expect(service).to receive(:create).and_call_original + expect(service).not_to receive(:execute_widgets) + .with(callback: :after_create_in_transaction, widget_params: widget_params) + + service_result + end end context 'checking spam' do @@ -80,5 +102,104 @@ RSpec.describe WorkItems::CreateService do service_result end end + + it_behaves_like 'work item widgetable service' do + let(:widget_params) do + { + hierarchy_widget: { parent: parent } + } + end + + let(:service) do + described_class.new( + project: project, + current_user: current_user, + params: opts, + spam_params: spam_params, + widget_params: widget_params + ) + end + + let(:service_execute) { service.execute } + + let(:supported_widgets) do + [ + { + klass: WorkItems::Widgets::HierarchyService::CreateService, + callback: :after_create_in_transaction, + params: { parent: parent } + } + ] + end + end + + describe 'hierarchy widget' do + let(:widget_params) { { hierarchy_widget: { parent: parent } } } + + shared_examples 'fails creating work item and returns errors' do + it 'does not create new work item if parent can not be set' do + expect { service_result }.not_to change(WorkItem, :count) + + expect(service_result[:status]).to be(:error) + expect(service_result[:message]).to match(error_message) + end + end + + context 'when user can admin parent link' do + let(:current_user) { reporter } + + context 'when parent is valid work item' do + let(:opts) do + { + title: 'Awesome work_item', + description: 'please fix', + work_item_type: create(:work_item_type, :task) + } + end + + it 'creates new work item and sets parent reference' do + expect { service_result }.to change( + WorkItem, :count).by(1).and(change( + WorkItems::ParentLink, :count).by(1)) + + expect(service_result[:status]).to be(:success) + end + end + + context 'when parent type is invalid' do + let_it_be(:parent) { create(:work_item, :task, project: project) } + + it_behaves_like 'fails creating work item and returns errors' do + let(:error_message) { 'only Issue and Incident can be parent of Task.'} + end + end + + context 'when hierarchy feature flag is disabled' do + before do + stub_feature_flags(work_items_hierarchy: false) + end + + it_behaves_like 'fails creating work item and returns errors' do + let(:error_message) { '`work_items_hierarchy` feature flag disabled for this project' } + end + end + end + + context 'when user cannot admin parent link' do + let(:current_user) { guest } + + let(:opts) do + { + title: 'Awesome work_item', + description: 'please fix', + work_item_type: create(:work_item_type, :task) + } + end + + it_behaves_like 'fails creating work item and returns errors' do + let(:error_message) { 'No matching task found. Make sure that you are adding a valid task ID.'} + end + end + end end end diff --git a/spec/services/work_items/delete_task_service_spec.rb b/spec/services/work_items/delete_task_service_spec.rb index 04944645c9b..07a0d8d6c1a 100644 --- a/spec/services/work_items/delete_task_service_spec.rb +++ b/spec/services/work_items/delete_task_service_spec.rb @@ -67,7 +67,7 @@ RSpec.describe WorkItems::DeleteTaskService do it 'removes the task list item with the work item reference' do expect do service_result - end.to change(list_work_item, :description).from(list_work_item.description).to('') + end.to change(list_work_item, :description).from(list_work_item.description).to("- [ ] #{task.title}") end end diff --git a/spec/services/work_items/parent_links/create_service_spec.rb b/spec/services/work_items/parent_links/create_service_spec.rb new file mode 100644 index 00000000000..85b0ee040cd --- /dev/null +++ b/spec/services/work_items/parent_links/create_service_spec.rb @@ -0,0 +1,173 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe WorkItems::ParentLinks::CreateService do + describe '#execute' do + let_it_be(:user) { create(:user) } + let_it_be(:guest) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:work_item) { create(:work_item, project: project) } + let_it_be(:task) { create(:work_item, :task, project: project) } + let_it_be(:task1) { create(:work_item, :task, project: project) } + let_it_be(:task2) { create(:work_item, :task, project: project) } + let_it_be(:guest_task) { create(:work_item, :task) } + let_it_be(:invalid_task) { build_stubbed(:work_item, :task, id: non_existing_record_id)} + let_it_be(:another_project) { (create :project) } + let_it_be(:other_project_task) { create(:work_item, :task, iid: 100, project: another_project) } + let_it_be(:existing_parent_link) { create(:parent_link, work_item: task, work_item_parent: work_item)} + + let(:parent_link_class) { WorkItems::ParentLink } + let(:issuable_type) { :task } + let(:params) { {} } + + before do + project.add_reporter(user) + project.add_guest(guest) + guest_task.project.add_guest(user) + another_project.add_reporter(user) + end + + shared_examples 'returns not found error' do + it 'returns error' do + error = "No matching #{issuable_type} found. Make sure that you are adding a valid #{issuable_type} ID." + + is_expected.to eq(service_error(error)) + end + + it 'no relationship is created' do + expect { subject }.not_to change(parent_link_class, :count) + end + end + + subject { described_class.new(work_item, user, params).execute } + + context 'when the reference list is empty' do + let(:params) { { issuable_references: [] } } + + it_behaves_like 'returns not found error' + end + + context 'when work item not found' do + let(:params) { { issuable_references: [invalid_task] } } + + it_behaves_like 'returns not found error' + end + + context 'when user has no permission to link work items' do + let(:params) { { issuable_references: [guest_task] } } + + it_behaves_like 'returns not found error' + end + + context 'child and parent are the same work item' do + let(:params) { { issuable_references: [work_item] } } + + it 'no relationship is created' do + expect { subject }.not_to change(parent_link_class, :count) + end + end + + context 'when there are tasks to relate' do + let(:params) { { issuable_references: [task1, task2] } } + + it 'creates relationships', :aggregate_failures do + expect { subject }.to change(parent_link_class, :count).by(2) + + tasks_parent = parent_link_class.where(work_item: [task1, task2]).map(&:work_item_parent).uniq + expect(tasks_parent).to match_array([work_item]) + end + + it 'returns success status and created links', :aggregate_failures do + expect(subject.keys).to match_array([:status, :created_references]) + expect(subject[:status]).to eq(:success) + expect(subject[:created_references].map(&:work_item_id)).to match_array([task1.id, task2.id]) + end + + context 'when task is already assigned' do + let(:params) { { issuable_references: [task, task2] } } + + it 'creates links only for non related tasks' do + expect { subject }.to change(parent_link_class, :count).by(1) + + expect(subject[:created_references].map(&:work_item_id)).to match_array([task2.id]) + end + end + + context 'when there are invalid children' do + let_it_be(:issue) { create(:work_item, project: project) } + + let(:params) { { issuable_references: [task1, issue, other_project_task] } } + + it 'creates links only for valid children' do + expect { subject }.to change { parent_link_class.count }.by(1) + end + + it 'returns error status' do + error = "#{issue.to_reference} cannot be added: only Task can be assigned as a child in hierarchy.. " \ + "#{other_project_task.to_reference} cannot be added: parent must be in the same project as child." + + is_expected.to eq(service_error(error, http_status: 422)) + end + end + + context 'when parent type is invalid' do + let(:work_item) { create :work_item, :task, project: project } + + let(:params) { { target_issuable: task1 } } + + it 'returns error status' do + error = "#{task1.to_reference} cannot be added: only Issue and Incident can be parent of Task." + + is_expected.to eq(service_error(error, http_status: 422)) + end + end + + context 'when max depth is reached' do + let(:params) { { issuable_references: [task2] } } + + before do + stub_const("#{parent_link_class}::MAX_CHILDREN", 1) + end + + it 'returns error status' do + error = "#{task2.to_reference} cannot be added: parent already has maximum number of children." + + is_expected.to eq(service_error(error, http_status: 422)) + end + end + + context 'when params include invalid ids' do + let(:params) { { issuable_references: [task1, invalid_task] } } + + it 'creates links only for valid IDs' do + expect { subject }.to change(parent_link_class, :count).by(1) + end + end + + context 'when user is a guest' do + let(:user) { guest } + + it_behaves_like 'returns not found error' + end + + context 'when user is a guest assigned to the work item' do + let(:user) { guest } + + before do + work_item.assignees = [guest] + end + + it_behaves_like 'returns not found error' + end + end + end + + def service_error(message, http_status: 404) + { + message: message, + status: :error, + http_status: http_status + } + end +end diff --git a/spec/services/work_items/parent_links/destroy_service_spec.rb b/spec/services/work_items/parent_links/destroy_service_spec.rb new file mode 100644 index 00000000000..574b70af397 --- /dev/null +++ b/spec/services/work_items/parent_links/destroy_service_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe WorkItems::ParentLinks::DestroyService do + describe '#execute' do + let_it_be(:reporter) { create(:user) } + let_it_be(:guest) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:work_item) { create(:work_item, project: project) } + let_it_be(:task) { create(:work_item, :task, project: project) } + let_it_be(:parent_link) { create(:parent_link, work_item: task, work_item_parent: work_item)} + + let(:parent_link_class) { WorkItems::ParentLink } + + subject { described_class.new(parent_link, user).execute } + + before do + project.add_reporter(reporter) + project.add_guest(guest) + end + + context 'when user has permissions to update work items' do + let(:user) { reporter } + + it 'removes relation' do + expect { subject }.to change(parent_link_class, :count).by(-1) + end + + it 'returns success message' do + is_expected.to eq(message: 'Relation was removed', status: :success) + end + end + + context 'when user has insufficient permissions' do + let(:user) { guest } + + it 'does not remove relation' do + expect { subject }.not_to change(parent_link_class, :count).from(1) + end + + it 'returns error message' do + is_expected.to eq(message: 'No Work Item Link found', status: :error, http_status: 404) + end + end + end +end diff --git a/spec/services/work_items/task_list_reference_removal_service_spec.rb b/spec/services/work_items/task_list_reference_removal_service_spec.rb index bca72da0efa..91b7814ae92 100644 --- a/spec/services/work_items/task_list_reference_removal_service_spec.rb +++ b/spec/services/work_items/task_list_reference_removal_service_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe WorkItems::TaskListReferenceRemovalService do let_it_be(:developer) { create(:user) } let_it_be(:project) { create(:project, :repository).tap { |project| project.add_developer(developer) } } - let_it_be(:task) { create(:work_item, project: project) } + let_it_be(:task) { create(:work_item, project: project, title: 'Task title') } let_it_be(:single_line_work_item, refind: true) do create(:work_item, project: project, description: "- [ ] #{task.to_reference}+ single line") end @@ -82,7 +82,7 @@ RSpec.describe WorkItems::TaskListReferenceRemovalService do let(:line_number_end) { 1 } let(:work_item) { single_line_work_item } - it_behaves_like 'successful work item task reference removal service', '' + it_behaves_like 'successful work item task reference removal service', '- [ ] Task title single line' context 'when description does not contain a task' do let_it_be(:no_matching_work_item) { create(:work_item, project: project, description: 'no matching task') } @@ -102,7 +102,8 @@ RSpec.describe WorkItems::TaskListReferenceRemovalService do end context 'when task mardown spans multiple lines' do - it_behaves_like 'successful work item task reference removal service', "Any text\n\n* [x] task\n\nMore text" + it_behaves_like 'successful work item task reference removal service', + "Any text\n\n* [ ] Item to be converted\n Task title second line\n third line\n* [x] task\n\nMore text" end context 'when updating the work item fails' do diff --git a/spec/services/work_items/task_list_reference_replacement_service_spec.rb b/spec/services/work_items/task_list_reference_replacement_service_spec.rb index e7914eb4a92..965c5f1d554 100644 --- a/spec/services/work_items/task_list_reference_replacement_service_spec.rb +++ b/spec/services/work_items/task_list_reference_replacement_service_spec.rb @@ -3,7 +3,8 @@ require 'spec_helper' RSpec.describe WorkItems::TaskListReferenceReplacementService do - let_it_be(:project) { create(:project, :repository) } + let_it_be(:developer) { create(:user) } + let_it_be(:project) { create(:project, :repository).tap { |project| project.add_developer(developer) } } let_it_be(:single_line_work_item, refind: true) { create(:work_item, project: project, description: '- [ ] single line', lock_version: 3) } let_it_be(:multiple_line_work_item, refind: true) { create(:work_item, project: project, description: "Any text\n\n* [ ] Item to be converted\n second line\n third line", lock_version: 3) } @@ -37,6 +38,7 @@ RSpec.describe WorkItems::TaskListReferenceReplacementService do subject(:result) do described_class.new( work_item: work_item, + current_user: developer, work_item_reference: reference, line_number_start: line_number_start, line_number_end: line_number_end, @@ -52,6 +54,12 @@ RSpec.describe WorkItems::TaskListReferenceReplacementService do let(:task_prefix) { '- [ ]' } it_behaves_like 'successful work item task reference replacement service' + + it 'creates description version note' do + expect { result }.to change(Note, :count).by(1) + expect(work_item.notes.last.note).to eq('changed the description') + expect(work_item.saved_description_version.id).to eq(work_item.notes.last.system_note_metadata.description_version_id) + end end context 'when task mardown spans multiple lines' do diff --git a/spec/services/work_items/update_service_spec.rb b/spec/services/work_items/update_service_spec.rb index 9030326dadb..b17c9ffb4fb 100644 --- a/spec/services/work_items/update_service_spec.rb +++ b/spec/services/work_items/update_service_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe WorkItems::UpdateService do let_it_be(:developer) { create(:user) } let_it_be(:project) { create(:project).tap { |proj| proj.add_developer(developer) } } + let_it_be(:parent) { create(:work_item, project: project) } let_it_be_with_reload(:work_item) { create(:work_item, project: project, assignees: [developer]) } let(:spam_params) { double } @@ -13,7 +14,15 @@ RSpec.describe WorkItems::UpdateService do let(:current_user) { developer } describe '#execute' do - subject(:update_work_item) { described_class.new(project: project, current_user: current_user, params: opts, spam_params: spam_params, widget_params: widget_params).execute(work_item) } + subject(:update_work_item) do + described_class.new( + project: project, + current_user: current_user, + params: opts, + spam_params: spam_params, + widget_params: widget_params + ).execute(work_item) + end before do stub_spam_services @@ -27,8 +36,7 @@ RSpec.describe WorkItems::UpdateService do expect(Gitlab::UsageDataCounters::WorkItemActivityUniqueCounter).to receive(:track_work_item_title_changed_action).with(author: current_user) # During the work item transition we also want to track work items as issues expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).to receive(:track_issue_title_changed_action) - - update_work_item + expect(update_work_item[:status]).to eq(:success) end end @@ -38,8 +46,7 @@ RSpec.describe WorkItems::UpdateService do it 'does not trigger issuable_title_updated graphql subscription' do expect(GraphqlTriggers).not_to receive(:issuable_title_updated) expect(Gitlab::UsageDataCounters::WorkItemActivityUniqueCounter).not_to receive(:track_work_item_title_changed_action) - - update_work_item + expect(update_work_item[:status]).to eq(:success) end end @@ -71,16 +78,104 @@ RSpec.describe WorkItems::UpdateService do end end + it_behaves_like 'work item widgetable service' do + let(:widget_params) do + { + hierarchy_widget: { parent: parent }, + description_widget: { description: 'foo' }, + weight_widget: { weight: 1 } + } + end + + let(:service) do + described_class.new( + project: project, + current_user: current_user, + params: opts, + spam_params: spam_params, + widget_params: widget_params + ) + end + + let(:service_execute) { service.execute(work_item) } + + let(:supported_widgets) do + [ + { klass: WorkItems::Widgets::DescriptionService::UpdateService, callback: :update, params: { description: 'foo' } }, + { klass: WorkItems::Widgets::WeightService::UpdateService, callback: :update, params: { weight: 1 } }, + { klass: WorkItems::Widgets::HierarchyService::UpdateService, callback: :before_update_in_transaction, params: { parent: parent } } + ] + end + end + context 'when updating widgets' do - context 'for the description widget' do - let(:widget_params) { { description_widget: { description: 'changed' } } } + let(:widget_service_class) { WorkItems::Widgets::DescriptionService::UpdateService } + let(:widget_params) { { description_widget: { description: 'changed' } } } + + context 'when widget service is not present' do + before do + allow(widget_service_class).to receive(:new).and_return(nil) + end + + it 'ignores widget param' do + expect { update_work_item }.not_to change(work_item, :description) + end + end + context 'when the widget does not support update callback' do + before do + allow_next_instance_of(widget_service_class) do |instance| + allow(instance) + .to receive(:update) + .with(params: { description: 'changed' }).and_return(nil) + end + end + + it 'ignores widget param' do + expect { update_work_item }.not_to change(work_item, :description) + end + end + + context 'for the description widget' do it 'updates the description of the work item' do update_work_item expect(work_item.description).to eq('changed') end end + + context 'for the hierarchy widget' do + let(:opts) { { title: 'changed' } } + let_it_be(:child_work_item) { create(:work_item, :task, project: project) } + + let(:widget_params) { { hierarchy_widget: { children: [child_work_item] } } } + + it 'updates the children of the work item' do + expect do + update_work_item + work_item.reload + end.to change(WorkItems::ParentLink, :count).by(1) + + expect(work_item.work_item_children).to include(child_work_item) + end + + context 'when child type is invalid' do + let_it_be(:child_work_item) { create(:work_item, project: project) } + + it 'returns error status' do + expect(subject[:status]).to be(:error) + expect(subject[:message]) + .to match("#{child_work_item.to_reference} cannot be added: only Task can be assigned as a child in hierarchy.") + end + + it 'does not update work item attributes' do + expect do + update_work_item + work_item.reload + end.to not_change(WorkItems::ParentLink, :count).and(not_change(work_item, :title)) + end + end + end end end end diff --git a/spec/services/work_items/widgets/description_service/update_service_spec.rb b/spec/services/work_items/widgets/description_service/update_service_spec.rb new file mode 100644 index 00000000000..a2eceb97f09 --- /dev/null +++ b/spec/services/work_items/widgets/description_service/update_service_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe WorkItems::Widgets::DescriptionService::UpdateService 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, description: 'old description') } + + let(:widget) { work_item.widgets.find {|widget| widget.is_a?(WorkItems::Widgets::Description) } } + + describe '#update' do + subject { described_class.new(widget: widget, current_user: user).update(params: params) } # rubocop:disable Rails/SaveBang + + context 'when description param is present' do + let(:params) { { description: 'updated description' } } + + it 'correctly sets work item description value' do + subject + + expect(work_item.description).to eq('updated description') + end + end + + context 'when description param is not present' do + let(:params) { {} } + + it 'does not change work item description value' do + subject + + expect(work_item.description).to eq('old description') + end + end + end +end diff --git a/spec/services/work_items/widgets/hierarchy_service/update_service_spec.rb b/spec/services/work_items/widgets/hierarchy_service/update_service_spec.rb new file mode 100644 index 00000000000..4f6ff1b8676 --- /dev/null +++ b/spec/services/work_items/widgets/hierarchy_service/update_service_spec.rb @@ -0,0 +1,160 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe WorkItems::Widgets::HierarchyService::UpdateService do + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + + let_it_be(:work_item) { create(:work_item, project: project) } + let_it_be(:parent_work_item) { create(:work_item, project: project) } + let_it_be(:child_work_item) { create(:work_item, :task, project: project) } + let_it_be(:existing_link) { create(:parent_link, work_item: child_work_item, work_item_parent: work_item) } + + let(:widget) { work_item.widgets.find {|widget| widget.is_a?(WorkItems::Widgets::Hierarchy) } } + let(:not_found_error) { 'No matching task found. Make sure that you are adding a valid task ID.' } + + shared_examples 'raises a WidgetError' do + it { expect { subject }.to raise_error(described_class::WidgetError, message) } + end + + describe '#update' do + subject { described_class.new(widget: widget, current_user: user).before_update_in_transaction(params: params) } + + context 'when parent and children params are present' do + let(:params) { { parent: parent_work_item, children: [child_work_item] } } + + it_behaves_like 'raises a WidgetError' do + let(:message) { 'A Work Item can be a parent or a child, but not both.' } + end + end + + context 'when updating children' do + let_it_be(:child_work_item2) { create(:work_item, :task, project: project) } + let_it_be(:child_work_item3) { create(:work_item, :task, project: project) } + let_it_be(:child_work_item4) { create(:work_item, :task, project: project) } + + context 'when work_items_hierarchy feature flag is disabled' do + let(:params) { { children: [child_work_item4] }} + + before do + stub_feature_flags(work_items_hierarchy: false) + end + + it_behaves_like 'raises a WidgetError' do + let(:message) { '`work_items_hierarchy` feature flag disabled for this project' } + end + end + + context 'when user has insufficient permissions to link work items' do + let(:params) { { children: [child_work_item4] }} + + it_behaves_like 'raises a WidgetError' do + let(:message) { not_found_error } + end + end + + context 'when user has sufficient permissions to link work item' do + before do + project.add_developer(user) + end + + context 'with valid params' do + let(:params) { { children: [child_work_item2, child_work_item3] }} + + it 'correctly sets work item parent' do + subject + + expect(work_item.reload.work_item_children) + .to contain_exactly(child_work_item, child_work_item2, child_work_item3) + end + end + + context 'when child is already assigned' do + let(:params) { { children: [child_work_item] }} + + it_behaves_like 'raises a WidgetError' do + let(:message) { 'Task(s) already assigned' } + end + end + + context 'when child type is invalid' do + let_it_be(:child_issue) { create(:work_item, project: project) } + + let(:params) { { children: [child_issue] }} + + it_behaves_like 'raises a WidgetError' do + let(:message) do + "#{child_issue.to_reference} cannot be added: only Task can be assigned as a child in hierarchy." + end + end + end + end + end + + context 'when updating parent' do + let_it_be(:work_item) { create(:work_item, :task, project: project) } + + let(:params) {{ parent: parent_work_item } } + + context 'when work_items_hierarchy feature flag is disabled' do + before do + stub_feature_flags(work_items_hierarchy: false) + end + + it_behaves_like 'raises a WidgetError' do + let(:message) { '`work_items_hierarchy` feature flag disabled for this project' } + end + end + + context 'when user has insufficient permissions to link work items' do + it_behaves_like 'raises a WidgetError' do + let(:message) { not_found_error } + end + end + + context 'when user has sufficient permissions to link work item' do + before do + project.add_developer(user) + end + + it 'correctly sets new parent' do + expect(subject[:status]).to eq(:success) + expect(work_item.work_item_parent).to eq(parent_work_item) + end + + context 'when parent is nil' do + let(:params) { { parent: nil } } + + it 'removes the work item parent if present' do + work_item.update!(work_item_parent: parent_work_item) + + expect do + subject + work_item.reload + end.to change(work_item, :work_item_parent).from(parent_work_item).to(nil) + end + + it 'returns success status if parent not present', :aggregate_failure do + work_item.update!(work_item_parent: nil) + + expect(subject[:status]).to eq(:success) + expect(work_item.reload.work_item_parent).to be_nil + end + end + + context 'when type is invalid' do + let_it_be(:parent_task) { create(:work_item, :task, project: project)} + + let(:params) {{ parent: parent_task } } + + it_behaves_like 'raises a WidgetError' do + let(:message) do + "#{work_item.to_reference} cannot be added: only Issue and Incident can be parent of Task." + end + end + end + end + end + end +end diff --git a/spec/services/work_items/widgets/weight_service/update_service_spec.rb b/spec/services/work_items/widgets/weight_service/update_service_spec.rb new file mode 100644 index 00000000000..97e17f1c526 --- /dev/null +++ b/spec/services/work_items/widgets/weight_service/update_service_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe WorkItems::Widgets::WeightService::UpdateService 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, weight: 1) } + + let(:widget) { work_item.widgets.find {|widget| widget.is_a?(WorkItems::Widgets::Weight) } } + + describe '#update' do + subject { described_class.new(widget: widget, current_user: user).update(params: params) } # rubocop:disable Rails/SaveBang + + context 'when weight param is present' do + let(:params) { { weight: 2 } } + + it 'correctly sets work item weight value' do + subject + + expect(work_item.weight).to eq(2) + end + end + + context 'when weight param is not present' do + let(:params) { {} } + + it 'does not change work item weight value', :aggregate_failures do + expect { subject } + .to not_change { work_item.weight } + + expect(work_item.weight).to eq(1) + end + end + end +end |