From d68be8df062a2d98c5a4e1681fb2a6b13f03ea62 Mon Sep 17 00:00:00 2001
From: Jan Provaznik <jprovaznik@gitlab.com>
Date: Tue, 13 Dec 2022 10:01:33 +0000
Subject: [PATCH] Fix setting of work item type

* Work item type is set in BuildService when creating an issue, we
  should avoid setting the type from params in later phase.
* allows creation of key results
* add okrs licence check
---
 app/services/issues/base_service.rb           |  5 ++
 ee/app/policies/ee/project_policy.rb          | 12 ++-
 ee/spec/policies/project_policy_spec.rb       | 15 +++-
 .../mutations/work_items/create_spec.rb       | 73 ++++++++++++++++++-
 .../api/graphql/project/work_items_spec.rb    |  2 +-
 spec/factories/work_items/work_item_types.rb  |  2 +-
 .../mutations/work_items/update_spec.rb       |  4 +-
 spec/services/issues/create_service_spec.rb   | 24 +++++-
 8 files changed, 123 insertions(+), 14 deletions(-)

diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb
index 28ea6b0ebf82f..10407e997154a 100644
--- a/app/services/issues/base_service.rb
+++ b/app/services/issues/base_service.rb
@@ -114,6 +114,11 @@ def delete_milestone_total_issue_counter_cache(milestone)
 
       Milestones::IssuesCountService.new(milestone).delete_cache
     end
+
+    override :allowed_create_params
+    def allowed_create_params(params)
+      super(params).except(:issue_type, :work_item_type_id, :work_item_type)
+    end
   end
 end
 
diff --git a/ee/app/policies/ee/project_policy.rb b/ee/app/policies/ee/project_policy.rb
index 05b49f42eeb7b..0217061b0dded 100644
--- a/ee/app/policies/ee/project_policy.rb
+++ b/ee/app/policies/ee/project_policy.rb
@@ -162,6 +162,11 @@ module ProjectPolicy
         @subject.group && (@subject.group.membership_lock? || ::Gitlab::CurrentSettings.lock_memberships_to_ldap?)
       end
 
+      with_scope :subject
+      condition(:okrs_enabled) do
+        @subject.okrs_mvc_feature_flag_enabled? && @subject.feature_available?(:okrs)
+      end
+
       rule { membership_locked_via_parent_group }.policy do
         prevent :import_project_members_from_another_project
       end
@@ -186,8 +191,6 @@ module ProjectPolicy
         @user.read_code_for?(project)
       end
 
-      condition(:okrs_enabled, scope: :subject) { project&.okrs_mvc_feature_flag_enabled? }
-
       # Owners can be banned from their own project except for top-level group
       # owners. This exception is made at the service layer
       # (Users::Abuse::GitAbuse::NamespaceThrottleService) where the ban record
@@ -534,7 +537,10 @@ module ProjectPolicy
 
       rule { custom_roles_allowed & role_enables_read_code }.enable :read_code
 
-      rule { can?(:create_issue) & okrs_enabled }.enable :create_objective
+      rule { can?(:create_issue) & okrs_enabled }.policy do
+        enable :create_objective
+        enable :create_key_result
+      end
     end
 
     override :lookup_access_level!
diff --git a/ee/spec/policies/project_policy_spec.rb b/ee/spec/policies/project_policy_spec.rb
index d991bb724105d..ad0555fded125 100644
--- a/ee/spec/policies/project_policy_spec.rb
+++ b/ee/spec/policies/project_policy_spec.rb
@@ -2425,7 +2425,7 @@ def expect_private_project_permissions_as_if_non_member
   describe 'create_objective' do
     using RSpec::Parameterized::TableSyntax
 
-    let(:policy) { :create_objective }
+    let(:okr_policies) { [:create_objective, :create_key_result] }
 
     where(:role, :allowed) do
       :guest      | true
@@ -2442,10 +2442,11 @@ def expect_private_project_permissions_as_if_non_member
 
       before do
         enable_admin_mode!(current_user) if role == :admin
+        stub_licensed_features(okrs: true)
       end
 
       context 'when okrs_mvc feature flag is enabled' do
-        it { is_expected.to(allowed ? be_allowed(policy) : be_disallowed(policy)) }
+        it { is_expected.to(allowed ? be_allowed(*okr_policies) : be_disallowed(*okr_policies)) }
       end
 
       context 'when okrs_mvc feature flag is disabled' do
@@ -2453,7 +2454,15 @@ def expect_private_project_permissions_as_if_non_member
           stub_feature_flags(okrs_mvc: false)
         end
 
-        it { is_expected.to(be_disallowed(policy)) }
+        it { is_expected.to be_disallowed(*okr_policies) }
+      end
+
+      context 'when okrs license feature is not available' do
+        before do
+          stub_licensed_features(okrs: false)
+        end
+
+        it { is_expected.to be_disallowed(*okr_policies) }
       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 c522178891619..f2527e9de136a 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
@@ -11,12 +11,12 @@
 
   let(:mutation) { graphql_mutation(:workItemCreate, input.merge('projectPath' => project.full_path)) }
   let(:mutation_response) { graphql_mutation_response(:work_item_create) }
+  let(:widgets_response) { mutation_response['workItem']['widgets'] }
 
   context 'when user has permissions to create a work item' do
     let(:current_user) { developer }
 
     context 'with iteration widget input' do
-      let(:widgets_response) { mutation_response['workItem']['widgets'] }
       let(:fields) do
         <<~FIELDS
         workItem {
@@ -85,5 +85,76 @@
         end
       end
     end
+
+    context 'when creating a key result' do
+      let_it_be(:parent) { create(:work_item, :objective, project: project) }
+
+      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(:mutation) { graphql_mutation(:workItemCreate, input.merge('projectPath' => project.full_path), fields) }
+      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
   end
 end
diff --git a/ee/spec/requests/api/graphql/project/work_items_spec.rb b/ee/spec/requests/api/graphql/project/work_items_spec.rb
index cfc9923ae123f..cd3354bb55293 100644
--- a/ee/spec/requests/api/graphql/project/work_items_spec.rb
+++ b/ee/spec/requests/api/graphql/project/work_items_spec.rb
@@ -56,7 +56,7 @@
       end
 
       before do
-        stub_licensed_features(requirements: true)
+        stub_licensed_features(requirements: true, okrs: true)
       end
 
       it 'returns work items including status', :aggregate_failures do
diff --git a/spec/factories/work_items/work_item_types.rb b/spec/factories/work_items/work_item_types.rb
index 1b6137503d3a9..d36cb6260c623 100644
--- a/spec/factories/work_items/work_item_types.rb
+++ b/spec/factories/work_items/work_item_types.rb
@@ -13,7 +13,7 @@
 
       # Expect base_types to exist on the DB
       if type_base_attributes.slice(:namespace, :namespace_id).compact.empty?
-        WorkItems::Type.find_or_initialize_by(type_base_attributes).tap { |type| type.assign_attributes(attributes) }
+        WorkItems::Type.find_or_initialize_by(type_base_attributes)
       else
         WorkItems::Type.new(attributes)
       end
diff --git a/spec/requests/api/graphql/mutations/work_items/update_spec.rb b/spec/requests/api/graphql/mutations/work_items/update_spec.rb
index c71c2ae6c39b3..14cb18d04b85e 100644
--- a/spec/requests/api/graphql/mutations/work_items/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/work_items/update_spec.rb
@@ -632,7 +632,7 @@
     end
 
     context 'when unsupported widget input is sent' do
-      let_it_be(:test_case) { create(:work_item_type, :default, :test_case, name: 'some_test_case_name') }
+      let_it_be(:test_case) { create(:work_item_type, :default, :test_case) }
       let_it_be(:work_item) { create(:work_item, work_item_type: test_case, project: project) }
 
       let(:input) do
@@ -642,7 +642,7 @@
       end
 
       it_behaves_like 'a mutation that returns top-level errors',
-        errors: ["Following widget keys are not supported by some_test_case_name type: [:hierarchy_widget]"]
+        errors: ["Following widget keys are not supported by Test Case type: [:hierarchy_widget]"]
     end
   end
 end
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index 7b51f9db8cd6f..7ab2046b6be46 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -9,21 +9,22 @@
   let_it_be_with_reload(:project) { create(:project, :public, group: group) }
   let_it_be(:user) { create(:user) }
 
+  let(:opts) { { title: 'title' } }
   let(:spam_params) { double }
+  let(:service) { described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params) }
 
   it_behaves_like 'rate limited service' do
     let(:key) { :issues_create }
     let(:key_scope) { %i[project current_user external_author] }
     let(:application_limit_key) { :issues_create_limit }
     let(:created_model) { Issue }
-    let(:service) { described_class.new(project: project, current_user: user, params: { title: 'title' }, spam_params: double) }
   end
 
   describe '#execute' do
     let_it_be(:assignee) { create(:user) }
     let_it_be(:milestone) { create(:milestone, project: project) }
 
-    let(:result) { described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute }
+    let(:result) { service.execute }
     let(:issue) { result[:issue] }
 
     before do
@@ -54,6 +55,7 @@
 
       let(:opts) do
         { title: 'Awesome issue',
+          issue_type: :task,
           description: 'please fix',
           assignee_ids: [assignee.id],
           label_ids: labels.map(&:id),
@@ -118,10 +120,26 @@
         expect(issue.labels).to match_array(labels)
         expect(issue.milestone).to eq(milestone)
         expect(issue.due_date).to eq(Date.tomorrow)
-        expect(issue.work_item_type.base_type).to eq('issue')
+        expect(issue.work_item_type.base_type).to eq('task')
         expect(issue.issue_customer_relations_contacts).to be_empty
       end
 
+      context 'when the work item type is not allowed to create' do
+        before do
+          allow_next_instance_of(::Issues::BuildService) do |instance|
+            allow(instance).to receive(:create_issue_type_allowed?).twice.and_return(false)
+          end
+        end
+
+        it 'ignores the type and creates default issue' do
+          expect(result).to be_success
+          expect(issue).to be_persisted
+          expect(issue).to be_a(::Issue)
+          expect(issue.work_item_type.base_type).to eq('issue')
+          expect(issue.issue_type).to eq('issue')
+        end
+      end
+
       it 'calls NewIssueWorker with correct arguments' do
         expect(NewIssueWorker).to receive(:perform_async).with(Integer, user.id, 'Issue')
 
-- 
GitLab