diff options
Diffstat (limited to 'spec/models/work_item_spec.rb')
-rw-r--r-- | spec/models/work_item_spec.rb | 244 |
1 files changed, 236 insertions, 8 deletions
diff --git a/spec/models/work_item_spec.rb b/spec/models/work_item_spec.rb index 6aacaa3c119..5a525d83c3b 100644 --- a/spec/models/work_item_spec.rb +++ b/spec/models/work_item_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe WorkItem, feature_category: :portfolio_management do + using RSpec::Parameterized::TableSyntax + let_it_be(:reusable_project) { create(:project) } describe 'associations' do @@ -99,15 +101,28 @@ RSpec.describe WorkItem, feature_category: :portfolio_management do end end - describe '#supports_assignee?' do - let(:work_item) { build(:work_item, :task) } + describe '#get_widget' do + let(:work_item) { build(:work_item, description: 'foo') } - before do - allow(work_item.work_item_type).to receive(:supports_assignee?).and_return(false) + it 'returns widget object' do + expect(work_item.get_widget(:description)).to be_an_instance_of(WorkItems::Widgets::Description) end - it 'delegates the call to its work item type' do - expect(work_item.supports_assignee?).to be(false) + context 'when widget does not exist' do + it 'returns nil' do + expect(work_item.get_widget(:nop)).to be_nil + end + end + end + + describe '#supports_assignee?' do + Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter::WIDGETS_FOR_TYPE.each_pair do |base_type, widgets| + specify do + work_item = build(:work_item, base_type) + supports_assignee = widgets.include?(:assignees) + + expect(work_item.supports_assignee?).to eq(supports_assignee) + end end end @@ -117,7 +132,7 @@ RSpec.describe WorkItem, feature_category: :portfolio_management do subject { work_item.supported_quick_action_commands } it 'returns quick action commands supported for all work items' do - is_expected.to include(:title, :reopen, :close, :cc, :tableflip, :shrug) + is_expected.to include(:title, :reopen, :close, :cc, :tableflip, :shrug, :type) end context 'when work item supports the assignee widget' do @@ -127,7 +142,7 @@ RSpec.describe WorkItem, feature_category: :portfolio_management do end context 'when work item does not the assignee widget' do - let(:work_item) { build(:work_item, :incident) } + let(:work_item) { build(:work_item, :test_case) } it 'omits assignee related quick action commands' do is_expected.not_to include(:assign, :unassign, :reassign) @@ -163,6 +178,30 @@ RSpec.describe WorkItem, feature_category: :portfolio_management do end end + describe 'transform_quick_action_params' do + let(:work_item) { build(:work_item, :task) } + + subject(:transformed_params) do + work_item.transform_quick_action_params({ + title: 'bar', + assignee_ids: ['foo'] + }) + end + + it 'correctly separates widget params from regular params' do + expect(transformed_params).to eq({ + common: { + title: 'bar' + }, + widgets: { + assignees_widget: { + assignee_ids: ['foo'] + } + } + }) + end + end + describe 'callbacks' do describe 'record_create_action' do it 'records the creation action after saving' do @@ -290,6 +329,20 @@ RSpec.describe WorkItem, feature_category: :portfolio_management do end end + describe '#link_reference_pattern' do + let(:match_data) { described_class.link_reference_pattern.match(link_reference_url) } + + context 'with work item url' do + let(:link_reference_url) { 'http://localhost/namespace/project/-/work_items/1' } + + it 'matches with expected attributes' do + expect(match_data['namespace']).to eq('namespace') + expect(match_data['project']).to eq('project') + expect(match_data['work_item']).to eq('1') + end + end + end + context 'with hierarchy' do let_it_be(:type1) { create(:work_item_type, namespace: reusable_project.namespace) } let_it_be(:type2) { create(:work_item_type, namespace: reusable_project.namespace) } @@ -344,4 +397,179 @@ RSpec.describe WorkItem, feature_category: :portfolio_management do end end end + + describe '#allowed_work_item_type_change' do + let_it_be(:all_types) { WorkItems::Type::BASE_TYPES.keys } + + it 'is possible to change between all types', :aggregate_failures do + all_types.each do |type| + work_item = build(:work_item, type, project: reusable_project) + + (all_types - [type]).each do |new_type| + work_item.work_item_type_id = WorkItems::Type.default_by_type(new_type).id + + expect(work_item).to be_valid, "#{type} to #{new_type}" + end + end + end + + context 'with ParentLink relation' do + let_it_be(:old_type) { create(:work_item_type) } + let_it_be(:new_type) { create(:work_item_type) } + + context 'with hierarchy restrictions' do + let_it_be(:child_type) { create(:work_item_type) } + + let_it_be_with_reload(:parent) { create(:work_item, work_item_type: old_type, project: reusable_project) } + let_it_be_with_reload(:child) { create(:work_item, work_item_type: child_type, project: reusable_project) } + + let_it_be(:hierarchy_restriction) do + create(:hierarchy_restriction, parent_type: old_type, child_type: child_type) + end + + let_it_be(:link) { create(:parent_link, work_item_parent: parent, work_item: child) } + + context 'when child items restrict the type change' do + before do + parent.work_item_type = new_type + end + + context 'when child items are compatible with the new type' do + let_it_be(:hierarchy_restriction_new_type) do + create(:hierarchy_restriction, parent_type: new_type, child_type: child_type) + end + + it 'allows to change types' do + expect(parent).to be_valid + expect(parent.errors).to be_empty + end + end + + context 'when child items are not compatible with the new type' do + it 'does not allow to change types' do + expect(parent).not_to be_valid + expect(parent.errors[:work_item_type_id]) + .to include("cannot be changed to #{new_type.name} with these child item types.") + end + end + end + + context 'when the parent restricts the type change' do + before do + child.work_item_type = new_type + end + + it 'does not allow to change types' do + expect(child.valid?).to eq(false) + expect(child.errors[:work_item_type_id]) + .to include("cannot be changed to #{new_type.name} with #{parent.work_item_type.name} as parent type.") + end + end + end + + context 'with hierarchy depth restriction' do + let_it_be_with_reload(:item1) { create(:work_item, work_item_type: new_type, project: reusable_project) } + let_it_be_with_reload(:item2) { create(:work_item, work_item_type: new_type, project: reusable_project) } + let_it_be_with_reload(:item3) { create(:work_item, work_item_type: new_type, project: reusable_project) } + let_it_be_with_reload(:item4) { create(:work_item, work_item_type: new_type, project: reusable_project) } + + let_it_be(:hierarchy_restriction1) do + create(:hierarchy_restriction, parent_type: old_type, child_type: new_type) + end + + let_it_be(:hierarchy_restriction2) do + create(:hierarchy_restriction, parent_type: new_type, child_type: old_type) + end + + let_it_be_with_reload(:hierarchy_restriction3) do + create(:hierarchy_restriction, parent_type: new_type, child_type: new_type, maximum_depth: 4) + end + + let_it_be(:link1) { create(:parent_link, work_item_parent: item1, work_item: item2) } + let_it_be(:link2) { create(:parent_link, work_item_parent: item2, work_item: item3) } + let_it_be(:link3) { create(:parent_link, work_item_parent: item3, work_item: item4) } + + before do + hierarchy_restriction3.update!(maximum_depth: maximum_depth) + end + + shared_examples 'validates the depth correctly' do + before do + work_item.update!(work_item_type: old_type) + end + + context 'when it is valid' do + let(:maximum_depth) { 4 } + + it 'allows to change types' do + work_item.work_item_type = new_type + + expect(work_item).to be_valid + end + end + + context 'when it is not valid' do + let(:maximum_depth) { 3 } + + it 'does not allow to change types' do + work_item.work_item_type = new_type + + expect(work_item).not_to be_valid + expect(work_item.errors[:work_item_type_id]).to include("reached maximum depth") + end + end + end + + context 'with the highest ancestor' do + let_it_be_with_reload(:work_item) { item1 } + + it_behaves_like 'validates the depth correctly' + end + + context 'with a child item' do + let_it_be_with_reload(:work_item) { item2 } + + it_behaves_like 'validates the depth correctly' + end + + context 'with the last child item' do + let_it_be_with_reload(:work_item) { item4 } + + it_behaves_like 'validates the depth correctly' + end + + context 'when ancestor is still the old type' do + let_it_be(:hierarchy_restriction4) do + create(:hierarchy_restriction, parent_type: old_type, child_type: old_type) + end + + before do + item1.update!(work_item_type: old_type) + item2.update!(work_item_type: old_type) + end + + context 'when it exceeds maximum depth' do + let(:maximum_depth) { 2 } + + it 'does not allow to change types' do + item2.work_item_type = new_type + + expect(item2).not_to be_valid + expect(item2.errors[:work_item_type_id]).to include("reached maximum depth") + end + end + + context 'when it does not exceed maximum depth' do + let(:maximum_depth) { 3 } + + it 'does allow to change types' do + item2.work_item_type = new_type + + expect(item2).to be_valid + end + end + end + end + end + end end |