Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-07-20 18:40:28 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-07-20 18:40:28 +0300
commitb595cb0c1dec83de5bdee18284abe86614bed33b (patch)
tree8c3d4540f193c5ff98019352f554e921b3a41a72 /spec/services/work_items
parent2f9104a328fc8a4bddeaa4627b595166d24671d0 (diff)
Add latest changes from gitlab-org/gitlab@15-2-stable-eev15.2.0-rc42
Diffstat (limited to 'spec/services/work_items')
-rw-r--r--spec/services/work_items/create_and_link_service_spec.rb19
-rw-r--r--spec/services/work_items/create_from_task_service_spec.rb10
-rw-r--r--spec/services/work_items/create_service_spec.rb123
-rw-r--r--spec/services/work_items/delete_task_service_spec.rb2
-rw-r--r--spec/services/work_items/parent_links/create_service_spec.rb173
-rw-r--r--spec/services/work_items/parent_links/destroy_service_spec.rb47
-rw-r--r--spec/services/work_items/task_list_reference_removal_service_spec.rb7
-rw-r--r--spec/services/work_items/task_list_reference_replacement_service_spec.rb10
-rw-r--r--spec/services/work_items/update_service_spec.rb109
-rw-r--r--spec/services/work_items/widgets/description_service/update_service_spec.rb35
-rw-r--r--spec/services/work_items/widgets/hierarchy_service/update_service_spec.rb160
-rw-r--r--spec/services/work_items/widgets/weight_service/update_service_spec.rb36
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