From d054075713dd54c1a00a3d9de281be1a8fb686a1 Mon Sep 17 00:00:00 2001
From: Andy Schoenen <asoiron@gitlab.com>
Date: Mon, 22 Jul 2024 15:36:23 +0000
Subject: [PATCH] Add CreateSecurityPolicyProjectAsync mutation

This mutation allows to trigger the creation of a security policy
project. We also introduce the securityPolicyProjectCreated subscription.
It will be triggered as soon as the project is created.

Changelog: added
EE: true
---
 config/sidekiq_queues.yml                     |   2 +
 doc/api/graphql/reference/index.md            |  45 +++++++
 ee/app/graphql/ee/graphql_triggers.rb         |   8 ++
 ee/app/graphql/ee/types/mutation_type.rb      |   1 +
 ee/app/graphql/ee/types/subscription_type.rb  |   5 +
 .../create_security_policy_project_async.rb   |  29 +++++
 .../security/policy_project_created.rb        |  29 +++++
 .../security/policy_project_created.rb        |  25 ++++
 .../policy_project_created_status_enum.rb     |  15 +++
 ee/app/workers/all_queues.yml                 |   9 ++
 .../create_security_policy_project_worker.rb  |  51 ++++++++
 ee/spec/graphql/graphql_triggers_spec.rb      |  21 ++++
 ...eate_security_policy_project_async_spec.rb |  91 ++++++++++++++
 .../create_security_policy_project_spec.rb    |   2 +-
 .../security/policy_project_created_spec.rb   |  85 +++++++++++++
 .../security/policy_project_created/helper.rb |  42 +++++++
 ...ate_security_policy_project_worker_spec.rb | 116 ++++++++++++++++++
 17 files changed, 575 insertions(+), 1 deletion(-)
 create mode 100644 ee/app/graphql/mutations/security_policy/create_security_policy_project_async.rb
 create mode 100644 ee/app/graphql/subscriptions/security/policy_project_created.rb
 create mode 100644 ee/app/graphql/types/gitlab_subscriptions/security/policy_project_created.rb
 create mode 100644 ee/app/graphql/types/gitlab_subscriptions/security/policy_project_created_status_enum.rb
 create mode 100644 ee/app/workers/security/create_security_policy_project_worker.rb
 create mode 100644 ee/spec/graphql/mutations/security_policy/create_security_policy_project_async_spec.rb
 create mode 100644 ee/spec/graphql/subscriptions/security/policy_project_created_spec.rb
 create mode 100644 ee/spec/support/helpers/graphql/subscriptions/security/policy_project_created/helper.rb
 create mode 100644 ee/spec/workers/security/create_security_policy_project_worker_spec.rb

diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 7849507b10c6d..ba820a4eaa697 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -729,6 +729,8 @@
   - 1
 - - search_zoekt_project_transfer
   - 1
+- - security_create_security_policy_project
+  - 1
 - - security_delete_orchestration_configuration
   - 1
 - - security_generate_policy_violation_comment
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index eadd055e207a1..36300dbe099e7 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -8644,6 +8644,30 @@ Input type: `SecurityPolicyProjectCreateInput`
 | <a id="mutationsecuritypolicyprojectcreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
 | <a id="mutationsecuritypolicyprojectcreateproject"></a>`project` | [`Project`](#project) | Security Policy Project that was created. |
 
+### `Mutation.securityPolicyProjectCreateAsync`
+
+**Status:** Alpha. Creates and assigns a security policy project for the given project or group (`full_path`) async.
+
+DETAILS:
+**Introduced** in GitLab 17.3.
+**Status**: Experiment.
+
+Input type: `SecurityPolicyProjectCreateAsyncInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationsecuritypolicyprojectcreateasyncclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationsecuritypolicyprojectcreateasyncfullpath"></a>`fullPath` | [`String!`](#string) | Full path of the project or group. |
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationsecuritypolicyprojectcreateasyncclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationsecuritypolicyprojectcreateasyncerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+
 ### `Mutation.securityPolicyProjectUnassign`
 
 Unassigns the security policy project for the given project (`full_path`).
@@ -27990,6 +28014,18 @@ Represents policy violation for `license_scanning` report_type.
 | <a id="policylicensescanningviolationlicense"></a>`license` | [`String!`](#string) | License name. |
 | <a id="policylicensescanningviolationurl"></a>`url` | [`String`](#string) | URL of the license. |
 
+### `PolicyProjectCreated`
+
+Response of security policy creation.
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="policyprojectcreatederrormessage"></a>`errorMessage` | [`String`](#string) | Error message in case status is :error. |
+| <a id="policyprojectcreatedproject"></a>`project` | [`Project`](#project) | Security Policy Project that was created. |
+| <a id="policyprojectcreatedstatus"></a>`status` | [`PolicyProjectCreatedStatus`](#policyprojectcreatedstatus) | Status of the creation of the security policy project. |
+
 ### `PolicyScanFindingViolation`
 
 Represents policy violation for `scan_finding` report_type.
@@ -36004,6 +36040,15 @@ Pipeline security report finding sort values.
 | <a id="pipelinestatusenumwaiting_for_callback"></a>`WAITING_FOR_CALLBACK` | Pipeline is waiting for an external action. |
 | <a id="pipelinestatusenumwaiting_for_resource"></a>`WAITING_FOR_RESOURCE` | A resource (for example, a runner) that the pipeline requires to run is unavailable. |
 
+### `PolicyProjectCreatedStatus`
+
+Types of security policy project created status.
+
+| Value | Description |
+| ----- | ----------- |
+| <a id="policyprojectcreatedstatuserror"></a>`ERROR` | Creating the security policy project faild. |
+| <a id="policyprojectcreatedstatussuccess"></a>`SUCCESS` | Creating the security policy project was successful. |
+
 ### `PolicyViolationErrorType`
 
 | Value | Description |
diff --git a/ee/app/graphql/ee/graphql_triggers.rb b/ee/app/graphql/ee/graphql_triggers.rb
index 96957d5d79cbd..6105f39ff190b 100644
--- a/ee/app/graphql/ee/graphql_triggers.rb
+++ b/ee/app/graphql/ee/graphql_triggers.rb
@@ -53,6 +53,14 @@ def self.workflow_events_updated(checkpoint)
         ::GitlabSchema.subscriptions.trigger(:workflow_events_updated, { workflow_id: checkpoint.workflow.to_gid },
           checkpoint)
       end
+
+      def self.security_policy_project_created(container, status, security_policy_project, error_message)
+        ::GitlabSchema.subscriptions.trigger(
+          :security_policy_project_created,
+          { full_path: container.full_path },
+          { status: status, error_message: error_message, project: security_policy_project }
+        )
+      end
     end
   end
 end
diff --git a/ee/app/graphql/ee/types/mutation_type.rb b/ee/app/graphql/ee/types/mutation_type.rb
index 0a41d13590892..01fe14f16b783 100644
--- a/ee/app/graphql/ee/types/mutation_type.rb
+++ b/ee/app/graphql/ee/types/mutation_type.rb
@@ -115,6 +115,7 @@ def self.authorization_scopes
         mount_mutation ::Mutations::SecurityPolicy::AssignSecurityPolicyProject
         mount_mutation ::Mutations::SecurityPolicy::UnassignSecurityPolicyProject
         mount_mutation ::Mutations::SecurityPolicy::CreateSecurityPolicyProject
+        mount_mutation ::Mutations::SecurityPolicy::CreateSecurityPolicyProjectAsync, alpha: { milestone: '17.3' }
         mount_mutation ::Mutations::Security::CiConfiguration::ConfigureDependencyScanning
         mount_mutation ::Mutations::Security::CiConfiguration::ConfigureContainerScanning
         mount_mutation ::Mutations::Security::TrainingProviderUpdate
diff --git a/ee/app/graphql/ee/types/subscription_type.rb b/ee/app/graphql/ee/types/subscription_type.rb
index decad5529cbf0..29f572bdc9178 100644
--- a/ee/app/graphql/ee/types/subscription_type.rb
+++ b/ee/app/graphql/ee/types/subscription_type.rb
@@ -35,6 +35,11 @@ def self.authorization_scopes
         field :workflow_events_updated,
           subscription: ::Subscriptions::Ai::DuoWorkflows::WorkflowEventsUpdated, null: true,
           description: 'Triggered when the checkpoints/events of a workflow is updated.'
+
+        field :security_policy_project_created,
+          subscription: Subscriptions::Security::PolicyProjectCreated, null: true,
+          description: 'Triggered when the security policy project is created for a specific group or project.',
+          alpha: { milestone: '17.3' }
       end
     end
   end
diff --git a/ee/app/graphql/mutations/security_policy/create_security_policy_project_async.rb b/ee/app/graphql/mutations/security_policy/create_security_policy_project_async.rb
new file mode 100644
index 0000000000000..074a66199ed07
--- /dev/null
+++ b/ee/app/graphql/mutations/security_policy/create_security_policy_project_async.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Mutations
+  module SecurityPolicy # rubocop:disable Gitlab/BoundedContexts -- Matches CreateSecurityPolicyProject and should be fixed together
+    class CreateSecurityPolicyProjectAsync < BaseMutation
+      graphql_name 'SecurityPolicyProjectCreateAsync'
+      description '**Status:** Alpha. ' \
+        'Creates and assigns a security policy project for the given project or group (`full_path`) async'
+
+      include FindsProjectOrGroupForSecurityPolicies
+
+      authorize :update_security_orchestration_policy_project
+
+      argument :full_path, GraphQL::Types::String,
+        required: true,
+        description: 'Full path of the project or group.'
+
+      def resolve(args)
+        project_or_group = authorized_find!(**args)
+
+        ::Security::CreateSecurityPolicyProjectWorker.perform_async(project_or_group.full_path, current_user.id) # rubocop:disable CodeReuse/Worker -- This is meant to be a background job
+
+        {
+          errors: []
+        }
+      end
+    end
+  end
+end
diff --git a/ee/app/graphql/subscriptions/security/policy_project_created.rb b/ee/app/graphql/subscriptions/security/policy_project_created.rb
new file mode 100644
index 0000000000000..68f427dea2e69
--- /dev/null
+++ b/ee/app/graphql/subscriptions/security/policy_project_created.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Subscriptions # rubocop:disable Gitlab/BoundedContexts -- Subscriptions is a concept of GraphQL
+  module Security
+    class PolicyProjectCreated < ::Subscriptions::BaseSubscription
+      include Gitlab::Graphql::Laziness
+
+      payload_type Types::GitlabSubscriptions::Security::PolicyProjectCreated
+
+      argument :full_path, GraphQL::Types::String,
+        required: true,
+        description: 'Full path of the project or group.'
+
+      def authorized?(args)
+        container = Routable.find_by_full_path(args[:full_path])
+
+        Ability.allowed?(current_user, :update_security_orchestration_policy_project, container)
+      end
+
+      def update(_)
+        {
+          project: object[:project],
+          status: object[:status],
+          error_message: object[:error_message]
+        }
+      end
+    end
+  end
+end
diff --git a/ee/app/graphql/types/gitlab_subscriptions/security/policy_project_created.rb b/ee/app/graphql/types/gitlab_subscriptions/security/policy_project_created.rb
new file mode 100644
index 0000000000000..2da92d846633f
--- /dev/null
+++ b/ee/app/graphql/types/gitlab_subscriptions/security/policy_project_created.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Types
+  module GitlabSubscriptions
+    module Security
+      # rubocop:disable Graphql/AuthorizeTypes -- Authorization will be handled in subscription
+      class PolicyProjectCreated < ::Types::BaseObject
+        graphql_name 'PolicyProjectCreated'
+        description 'Response of security policy creation.'
+
+        field :project, Types::ProjectType,
+          null: true,
+          description: 'Security Policy Project that was created.'
+
+        field :status, Types::GitlabSubscriptions::Security::PolicyProjectCreatedStatusEnum,
+          description: 'Status of the creation of the security policy project.'
+
+        field :error_message, GraphQL::Types::String,
+          null: true,
+          description: 'Error message in case status is :error.'
+      end
+      # rubocop:enable Graphql/AuthorizeTypes
+    end
+  end
+end
diff --git a/ee/app/graphql/types/gitlab_subscriptions/security/policy_project_created_status_enum.rb b/ee/app/graphql/types/gitlab_subscriptions/security/policy_project_created_status_enum.rb
new file mode 100644
index 0000000000000..379ba6109a310
--- /dev/null
+++ b/ee/app/graphql/types/gitlab_subscriptions/security/policy_project_created_status_enum.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Types
+  module GitlabSubscriptions
+    module Security
+      class PolicyProjectCreatedStatusEnum < ::Types::BaseEnum
+        graphql_name 'PolicyProjectCreatedStatus'
+        description 'Types of security policy project created status.'
+
+        value 'SUCCESS', value: :success, description: 'Creating the security policy project was successful.'
+        value 'ERROR', value: :error, description: 'Creating the security policy project faild.'
+      end
+    end
+  end
+end
diff --git a/ee/app/workers/all_queues.yml b/ee/app/workers/all_queues.yml
index fe247edacba07..cddb498b3df8f 100644
--- a/ee/app/workers/all_queues.yml
+++ b/ee/app/workers/all_queues.yml
@@ -2118,6 +2118,15 @@
   :weight: 1
   :idempotent: true
   :tags: []
+- :name: security_create_security_policy_project
+  :worker_name: Security::CreateSecurityPolicyProjectWorker
+  :feature_category: :security_policy_management
+  :has_external_dependencies: false
+  :urgency: :high
+  :resource_boundary: :unknown
+  :weight: 1
+  :idempotent: true
+  :tags: []
 - :name: security_delete_orchestration_configuration
   :worker_name: Security::DeleteOrchestrationConfigurationWorker
   :feature_category: :security_policy_management
diff --git a/ee/app/workers/security/create_security_policy_project_worker.rb b/ee/app/workers/security/create_security_policy_project_worker.rb
new file mode 100644
index 0000000000000..015e1c33f1f2d
--- /dev/null
+++ b/ee/app/workers/security/create_security_policy_project_worker.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module Security
+  class CreateSecurityPolicyProjectWorker
+    include ApplicationWorker
+
+    feature_category :security_policy_management
+    data_consistency :sticky
+    urgency :high
+    deduplicate :until_executed # Avoid race condition in creating security policy projects
+    idempotent!
+
+    def perform(project_or_group_path, current_user_id)
+      container = Routable.find_by_full_path(project_or_group_path)
+      user = User.find_by_id(current_user_id)
+
+      errors = [].tap do |errors|
+        errors << 'Group or project not found.' if container.blank?
+        errors << 'User not found.' if user.blank?
+      end
+
+      if errors.any?
+        error(errors.join(' '), container)
+
+        return
+      end
+
+      service_result = ::Security::SecurityOrchestrationPolicies::ProjectCreateService
+        .new(container: container, current_user: user)
+        .execute
+
+      GraphqlTriggers.security_policy_project_created(
+        container,
+        service_result[:status],
+        service_result[:policy_project],
+        service_result[:message]
+      )
+    end
+
+    private
+
+    def error(message, container = nil)
+      GraphqlTriggers.security_policy_project_created(
+        container,
+        :error,
+        nil,
+        message
+      )
+    end
+  end
+end
diff --git a/ee/spec/graphql/graphql_triggers_spec.rb b/ee/spec/graphql/graphql_triggers_spec.rb
index a57bfc0448cf2..ba8c75a51863e 100644
--- a/ee/spec/graphql/graphql_triggers_spec.rb
+++ b/ee/spec/graphql/graphql_triggers_spec.rb
@@ -153,4 +153,25 @@
       ::GraphqlTriggers.workflow_events_updated(checkpoint)
     end
   end
+
+  describe '.security_policy_project_created' do
+    subject(:trigger) do
+      described_class.security_policy_project_created(container, status, security_policy_project, error_message)
+    end
+
+    let_it_be(:container) { create(:project) }
+    let_it_be(:security_policy_project) { create(:project) }
+    let(:status) { :success }
+    let(:error_message) { nil }
+
+    it 'triggers the subscription' do
+      expect(GitlabSchema.subscriptions).to receive(:trigger).with(
+        :security_policy_project_created,
+        { full_path: container.full_path },
+        { status: status, project: security_policy_project, error_message: error_message }
+      )
+
+      trigger
+    end
+  end
 end
diff --git a/ee/spec/graphql/mutations/security_policy/create_security_policy_project_async_spec.rb b/ee/spec/graphql/mutations/security_policy/create_security_policy_project_async_spec.rb
new file mode 100644
index 0000000000000..d142544d64d04
--- /dev/null
+++ b/ee/spec/graphql/mutations/security_policy/create_security_policy_project_async_spec.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::SecurityPolicy::CreateSecurityPolicyProjectAsync, feature_category: :security_policy_management do
+  let(:mutation) { described_class.new(object: nil, context: { current_user: current_user }, field: nil) }
+
+  describe '#resolve' do
+    let_it_be(:owner) { create(:user) }
+    let_it_be(:maintainer) { create(:user) }
+    let_it_be(:namespace) { create(:group) }
+    let_it_be(:project) { create(:project, namespace: namespace) }
+
+    let(:current_user) { owner }
+
+    subject(:resolve_mutation) { mutation.resolve(full_path: container.full_path) }
+
+    shared_examples 'triggers the create security policy project worker' do
+      context 'when licensed feature is available' do
+        before do
+          stub_licensed_features(security_orchestration_policies: true)
+        end
+
+        context 'when user is an owner of the container' do
+          let(:current_user) { owner }
+
+          before_all do
+            namespace.add_owner(owner)
+          end
+
+          it 'triggers the worker' do
+            expect(Security::CreateSecurityPolicyProjectWorker).to receive(:perform_async).with(
+              container.full_path,
+              current_user.id
+            )
+
+            result = resolve_mutation
+
+            expect(result[:errors]).to be_empty
+          end
+        end
+
+        context 'when user is not an owner' do
+          let(:current_user) { maintainer }
+
+          before do
+            container.add_maintainer(maintainer)
+          end
+
+          it 'raises exception' do
+            expect { resolve_mutation }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+          end
+        end
+      end
+
+      context 'when feature is not licensed' do
+        before do
+          stub_licensed_features(security_orchestration_policies: false)
+        end
+
+        it 'raises exception' do
+          expect { resolve_mutation }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+        end
+      end
+    end
+
+    context 'when fullPath is not provided' do
+      subject(:resolve_mutation) { mutation.resolve({}) }
+
+      before do
+        stub_licensed_features(security_orchestration_policies: true)
+      end
+
+      it 'raises exception' do
+        expect { resolve_mutation }.to raise_error(Gitlab::Graphql::Errors::ArgumentError)
+      end
+    end
+
+    context 'for project' do
+      let(:container) { project }
+
+      it_behaves_like 'triggers the create security policy project worker'
+    end
+
+    context 'for namespace' do
+      let(:container) { namespace }
+
+      it_behaves_like 'triggers the create security policy project worker'
+    end
+  end
+end
diff --git a/ee/spec/graphql/mutations/security_policy/create_security_policy_project_spec.rb b/ee/spec/graphql/mutations/security_policy/create_security_policy_project_spec.rb
index 3518b2ef96cda..bd1267d26fb34 100644
--- a/ee/spec/graphql/mutations/security_policy/create_security_policy_project_spec.rb
+++ b/ee/spec/graphql/mutations/security_policy/create_security_policy_project_spec.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 require 'spec_helper'
 
-RSpec.describe Mutations::SecurityPolicy::CreateSecurityPolicyProject do
+RSpec.describe Mutations::SecurityPolicy::CreateSecurityPolicyProject, feature_category: :security_policy_management do
   let(:mutation) { described_class.new(object: nil, context: { current_user: current_user }, field: nil) }
 
   describe '#resolve' do
diff --git a/ee/spec/graphql/subscriptions/security/policy_project_created_spec.rb b/ee/spec/graphql/subscriptions/security/policy_project_created_spec.rb
new file mode 100644
index 0000000000000..4be1d4727c597
--- /dev/null
+++ b/ee/spec/graphql/subscriptions/security/policy_project_created_spec.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe Subscriptions::Security::PolicyProjectCreated, feature_category: :security_policy_management do
+  include GraphqlHelpers
+  include ::Graphql::Subscriptions::Security::PolicyProjectCreated::Helper
+
+  let_it_be(:project) { create(:project) }
+  let_it_be(:security_policy_project) { create(:project) }
+  let_it_be(:user) { create(:user) }
+
+  let(:current_user) { nil }
+  let(:subscribe) { security_policy_project_created_subscription(project, current_user) }
+  let(:status) { :success }
+  let(:error_message) { nil }
+
+  let(:security_policy_project_created) do
+    graphql_dig_at(graphql_data(response[:result]), :securityPolicyProjectCreated)
+  end
+
+  before_all do
+    project.add_owner(user)
+  end
+
+  before do
+    stub_licensed_features(security_orchestration_policies: true)
+
+    stub_const('GitlabSchema', Graphql::Subscriptions::ActionCable::MockGitlabSchema)
+    Graphql::Subscriptions::ActionCable::MockActionCable.clear_mocks
+  end
+
+  subject(:response) do
+    subscription_response do
+      GraphqlTriggers.security_policy_project_created(project, status, security_policy_project, error_message)
+    end
+  end
+
+  context 'when user is unauthorized' do
+    it 'does not receive any data' do
+      expect(response).to be_nil
+    end
+  end
+
+  context 'when user is authorized' do
+    let(:current_user) { user }
+
+    it 'does not receive the security policy project' do
+      created_response = security_policy_project_created
+
+      expect(created_response['project']).to be_nil
+      expect(created_response['error_message']).to eq(nil)
+      expect(created_response['status']).to eq('SUCCESS')
+    end
+
+    context 'and user has access to the security policy project' do
+      before_all do
+        security_policy_project.add_owner(user)
+      end
+
+      it 'receives the security policy project' do
+        created_response = security_policy_project_created
+
+        expect(created_response['project']['name']).to eq(security_policy_project.name)
+        expect(created_response['error_message']).to eq(nil)
+        expect(created_response['status']).to eq('SUCCESS')
+      end
+
+      context 'and there is an error_message' do
+        let_it_be(:security_policy_project) { nil }
+
+        let(:error_message) { 'Error' }
+        let(:status) { :error }
+
+        it 'receives the error message' do
+          created_response = security_policy_project_created
+
+          expect(created_response['project']).to eq(nil)
+          expect(created_response['errorMessage']).to eq('Error')
+          expect(created_response['status']).to eq('ERROR')
+        end
+      end
+    end
+  end
+end
diff --git a/ee/spec/support/helpers/graphql/subscriptions/security/policy_project_created/helper.rb b/ee/spec/support/helpers/graphql/subscriptions/security/policy_project_created/helper.rb
new file mode 100644
index 0000000000000..27c1578bd9a58
--- /dev/null
+++ b/ee/spec/support/helpers/graphql/subscriptions/security/policy_project_created/helper.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module Graphql
+  module Subscriptions
+    module Security
+      module PolicyProjectCreated
+        module Helper
+          def subscription_response
+            subscription_channel = subscribe
+            yield
+            subscription_channel.mock_broadcasted_messages.first
+          end
+
+          def security_policy_project_created_subscription(container, current_user)
+            mock_channel = Graphql::Subscriptions::ActionCable::MockActionCable.get_mock_channel
+            query = security_policy_project_created_subscription_query(container)
+
+            GitlabSchema.execute(query, context: { current_user: current_user, channel: mock_channel })
+
+            mock_channel
+          end
+
+          private
+
+          def security_policy_project_created_subscription_query(container)
+            <<~SUBSCRIPTION
+              subscription {
+                securityPolicyProjectCreated(fullPath: \"#{container.full_path}\") {
+                  project {
+                    name
+                  }
+                  status
+                  errorMessage
+                }
+              }
+            SUBSCRIPTION
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/ee/spec/workers/security/create_security_policy_project_worker_spec.rb b/ee/spec/workers/security/create_security_policy_project_worker_spec.rb
new file mode 100644
index 0000000000000..9530cc57f847f
--- /dev/null
+++ b/ee/spec/workers/security/create_security_policy_project_worker_spec.rb
@@ -0,0 +1,116 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Security::CreateSecurityPolicyProjectWorker, "#perform", feature_category: :security_policy_management do
+  describe '#perform' do
+    let_it_be(:group) { create(:group) }
+    let_it_be(:project) { create(:project, group: group) }
+    let_it_be(:user) { create(:user) }
+
+    let(:user_id) { user.id }
+    let(:container_path) { project.full_path }
+
+    subject(:run_worker) { described_class.new.perform(container_path, user_id) }
+
+    before_all do
+      group.add_owner(user)
+    end
+
+    shared_examples 'a worker that does not call the ProjectCreateService' do
+      it 'does not call the ProjectCreateService' do
+        expect(Security::SecurityOrchestrationPolicies::ProjectCreateService).not_to receive(:new)
+      end
+    end
+
+    it 'calls the ProjectCreateService' do
+      expect_next_instance_of(
+        Security::SecurityOrchestrationPolicies::ProjectCreateService,
+        container: project,
+        current_user: user
+      ) do |service|
+        expect(service).to receive(:execute).and_call_original
+      end
+
+      run_worker
+    end
+
+    it 'triggers the security_policy_project_created GraphQL event' do
+      expect_next_instance_of(
+        Security::SecurityOrchestrationPolicies::ProjectCreateService,
+        container: project,
+        current_user: user
+      ) do |service|
+        expect(service).to receive(:execute).and_return({ status: :success, policy_project: 'a policy project' })
+      end
+
+      expect(GraphqlTriggers).to receive(:security_policy_project_created).with(
+        project, :success, "a policy project", nil)
+
+      run_worker
+    end
+
+    context 'when ProjectCreateService returns an error' do
+      specify do
+        expect_next_instance_of(
+          Security::SecurityOrchestrationPolicies::ProjectCreateService,
+          container: project,
+          current_user: user
+        ) do |service|
+          expect(service).to receive(:execute).and_return(
+            { status: :error, message: 'Security Policy project already exists.' }
+          )
+        end
+
+        expect(GraphqlTriggers).to receive(:security_policy_project_created).with(
+          project, :error, nil, 'Security Policy project already exists.'
+        )
+
+        run_worker
+      end
+    end
+
+    context 'when user can\'t be found' do
+      let(:user_id) { non_existing_record_id }
+
+      it_behaves_like 'a worker that does not call the ProjectCreateService'
+
+      it 'triggers the subscription with an error' do
+        expect(GraphqlTriggers).to receive(:security_policy_project_created).with(
+          project, :error, nil, 'User not found.'
+        )
+
+        run_worker
+      end
+    end
+
+    context 'when container can\'t be found' do
+      let(:container_path) { non_existing_record_id }
+
+      it_behaves_like 'a worker that does not call the ProjectCreateService'
+
+      it 'triggers the subscription with an error' do
+        expect(GraphqlTriggers).to receive(:security_policy_project_created).with(
+          nil, :error, nil, 'Group or project not found.'
+        )
+
+        run_worker
+      end
+    end
+
+    context 'when container and user can\'t be found' do
+      let(:container_path) { non_existing_record_id }
+      let(:user_id) { non_existing_record_id }
+
+      it_behaves_like 'a worker that does not call the ProjectCreateService'
+
+      it 'triggers the subscription with an error' do
+        expect(GraphqlTriggers).to receive(:security_policy_project_created).with(
+          nil, :error, nil, 'Group or project not found. User not found.'
+        )
+
+        run_worker
+      end
+    end
+  end
+end
-- 
GitLab