From b3fc0d54a0fb41233054d87d03db981574172f21 Mon Sep 17 00:00:00 2001
From: Daniel Duvall <dan@mutual.io>
Date: Thu, 4 Apr 2024 16:55:44 +0000
Subject: [PATCH] Fixed Phorge reference_pattern

Modified `Integrations::Phorge#reference_pattern` to use a regular
expression more closely resembling that of Phorge's own parser, and
documented the method's expected behavior as well as justifications for
the regex differences.

Implemented additional specs for cases where the pattern should and
should not match.
---
 app/models/integration.rb                     |  2 +-
 app/models/integrations/phorge.rb             | 58 ++++++++++++++++
 app/models/project.rb                         |  1 +
 .../20240229180548_projects_phorge_active.yml | 21 ++++++
 ...0823_projects_inheriting_phorge_active.yml | 21 ++++++
 ...20240301170844_instances_phorge_active.yml | 21 ++++++
 .../20240301170915_groups_phorge_active.yml   | 21 ++++++
 ...171403_groups_inheriting_phorge_active.yml | 21 ++++++
 doc/.vale/gitlab/spelling-exceptions.txt      |  2 +
 doc/api/graphql/reference/index.md            |  1 +
 doc/api/integrations.md                       | 35 ++++++++++
 doc/integration/external-issue-tracker.md     |  1 +
 doc/user/project/integrations/index.md        |  1 +
 doc/user/project/integrations/phorge.md       | 35 ++++++++++
 lib/api/helpers/integrations_helpers.rb       |  2 +
 locale/gitlab.pot                             |  6 ++
 spec/factories/integrations.rb                |  6 ++
 .../user_activates_issue_tracker_spec.rb      |  1 +
 .../types/projects/service_type_enum_spec.rb  |  1 +
 .../external_issue_reference_filter_spec.rb   | 17 +++++
 spec/lib/gitlab/import_export/all_models.yml  |  1 +
 spec/models/integrations/phorge_spec.rb       | 67 +++++++++++++++++++
 spec/models/project_spec.rb                   |  1 +
 23 files changed, 342 insertions(+), 1 deletion(-)
 create mode 100644 app/models/integrations/phorge.rb
 create mode 100644 config/metrics/counts_all/20240229180548_projects_phorge_active.yml
 create mode 100644 config/metrics/counts_all/20240301170823_projects_inheriting_phorge_active.yml
 create mode 100644 config/metrics/counts_all/20240301170844_instances_phorge_active.yml
 create mode 100644 config/metrics/counts_all/20240301170915_groups_phorge_active.yml
 create mode 100644 config/metrics/counts_all/20240301171403_groups_inheriting_phorge_active.yml
 create mode 100644 doc/user/project/integrations/phorge.md
 create mode 100644 spec/models/integrations/phorge_spec.rb

diff --git a/app/models/integration.rb b/app/models/integration.rb
index 50a3f53cfdcc2..51750ad6ab7e7 100644
--- a/app/models/integration.rb
+++ b/app/models/integration.rb
@@ -22,7 +22,7 @@ class Integration < ApplicationRecord
     asana assembla bamboo bugzilla buildkite campfire clickup confluence custom_issue_tracker
     datadog diffblue_cover discord drone_ci emails_on_push ewm external_wiki
     gitlab_slack_application hangouts_chat harbor irker jira
-    mattermost mattermost_slash_commands microsoft_teams packagist pipelines_email
+    mattermost mattermost_slash_commands microsoft_teams packagist phorge pipelines_email
     pivotaltracker prometheus pumble pushover redmine slack slack_slash_commands squash_tm teamcity telegram
     unify_circuit webex_teams youtrack zentao
   ].freeze
diff --git a/app/models/integrations/phorge.rb b/app/models/integrations/phorge.rb
new file mode 100644
index 0000000000000..822cbd8d07748
--- /dev/null
+++ b/app/models/integrations/phorge.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+module Integrations
+  class Phorge < BaseIssueTracker
+    include HasIssueTrackerFields
+
+    validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated?
+
+    # See https://we.phorge.it/source/phorge/browse/master/src/infrastructure/markup/rule/PhabricatorObjectRemarkupRule.php
+    # for a canonical source of the regular expression used to parse Phorge
+    # object references.
+    #
+    # > The "(?<![#@-])" prevents us from linking "#abcdef" or similar, and
+    # > "ABC-T1" (see T5714), and from matching "@T1" as a task (it is a user)
+    # > (see T9479).
+    #
+    # Note that object references in Phorge are prefixed with letters unique
+    # to their underlying application, so T123 (a Maniphest task) is
+    # distinct from D123 (a Differential patch). Keeping the T as part of
+    # the task ID is appropriate here as it leaves room for expanding
+    # reference parsing/linking to other types of Phorge entities.
+    #
+    # Also note, a prefix of # is being allowed here due to: 1) an assumed
+    # likelihood of use; and b) lack of collision with native GitLab issues
+    # since all Phorge identifiers have the application specific alpha prefix.
+    def reference_pattern(*)
+      @reference_pattern ||= /\b(?<![@-])(?<issue>T\d+)\b/
+    end
+
+    def self.title
+      'Phorge'
+    end
+
+    def self.description
+      s_("IssueTracker|Use Phorge as this project's issue tracker.")
+    end
+
+    # rubocop:disable Rails/OutputSafety -- It is fine to call html_safe here
+    def self.help
+      docs_link = ActionController::Base.helpers.link_to _('Learn more.'),
+        Rails.application.routes.url_helpers.help_page_url('user/project/integrations/phorge'),
+        target: '_blank',
+        rel: 'noopener noreferrer'
+
+      # rubocop:disable Gitlab/Rails/SafeFormat -- See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/145863#note_1845580057
+      format(
+        s_("IssueTracker|Use Phorge as this project's issue tracker. %{docs_link}").html_safe,
+        docs_link: docs_link.html_safe
+      )
+      # rubocop:enable Gitlab/Rails/SafeFormat
+    end
+    # rubocop:enable Rails/OutputSafety
+
+    def self.to_param
+      'phorge'
+    end
+  end
+end
diff --git a/app/models/project.rb b/app/models/project.rb
index eb1a6922db5a3..76dbd0547bbb9 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -234,6 +234,7 @@ def self.integration_association_name(name)
   has_one :mock_ci_integration, class_name: 'Integrations::MockCi'
   has_one :mock_monitoring_integration, class_name: 'Integrations::MockMonitoring'
   has_one :packagist_integration, class_name: 'Integrations::Packagist'
+  has_one :phorge_integration, class_name: 'Integrations::Phorge'
   has_one :pipelines_email_integration, class_name: 'Integrations::PipelinesEmail'
   has_one :pivotaltracker_integration, class_name: 'Integrations::Pivotaltracker'
   has_one :prometheus_integration, class_name: 'Integrations::Prometheus', inverse_of: :project
diff --git a/config/metrics/counts_all/20240229180548_projects_phorge_active.yml b/config/metrics/counts_all/20240229180548_projects_phorge_active.yml
new file mode 100644
index 0000000000000..20da23f37a960
--- /dev/null
+++ b/config/metrics/counts_all/20240229180548_projects_phorge_active.yml
@@ -0,0 +1,21 @@
+---
+key_path: counts.projects_phorge_active
+description: Count of groups with active integrations for Phorge
+product_section: dev
+product_stage: manage
+product_group: integrations
+value_type: number
+status: active
+milestone: "16.11"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/145863
+time_frame: all
+data_source: database
+data_category: optional
+performance_indicator_type: []
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
diff --git a/config/metrics/counts_all/20240301170823_projects_inheriting_phorge_active.yml b/config/metrics/counts_all/20240301170823_projects_inheriting_phorge_active.yml
new file mode 100644
index 0000000000000..2ad59539695ee
--- /dev/null
+++ b/config/metrics/counts_all/20240301170823_projects_inheriting_phorge_active.yml
@@ -0,0 +1,21 @@
+---
+key_path: counts.projects_inheriting_phorge_active
+description: Count of active projects inheriting integrations for Phorge
+product_section: dev
+product_stage: manage
+product_group: integrations
+value_type: number
+status: active
+milestone: "16.11"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/145863
+time_frame: all
+data_source: database
+data_category: optional
+performance_indicator_type: []
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
diff --git a/config/metrics/counts_all/20240301170844_instances_phorge_active.yml b/config/metrics/counts_all/20240301170844_instances_phorge_active.yml
new file mode 100644
index 0000000000000..9ec941ab78142
--- /dev/null
+++ b/config/metrics/counts_all/20240301170844_instances_phorge_active.yml
@@ -0,0 +1,21 @@
+---
+key_path: counts.instances_phorge_active
+description: Count of active instance-level integrations for Phorge
+product_section: dev
+product_stage: manage
+product_group: integrations
+value_type: number
+status: active
+milestone: "16.11"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/145863
+time_frame: all
+data_source: database
+data_category: optional
+performance_indicator_type: []
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
diff --git a/config/metrics/counts_all/20240301170915_groups_phorge_active.yml b/config/metrics/counts_all/20240301170915_groups_phorge_active.yml
new file mode 100644
index 0000000000000..0471127bd2365
--- /dev/null
+++ b/config/metrics/counts_all/20240301170915_groups_phorge_active.yml
@@ -0,0 +1,21 @@
+---
+key_path: counts.groups_phorge_active
+description: Count of groups with active integrations for Phorge
+product_section: dev
+product_stage: manage
+product_group: integrations
+value_type: number
+status: active
+milestone: "16.11"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/145863
+time_frame: all
+data_source: database
+data_category: optional
+performance_indicator_type: []
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
diff --git a/config/metrics/counts_all/20240301171403_groups_inheriting_phorge_active.yml b/config/metrics/counts_all/20240301171403_groups_inheriting_phorge_active.yml
new file mode 100644
index 0000000000000..cb2fa8f789b12
--- /dev/null
+++ b/config/metrics/counts_all/20240301171403_groups_inheriting_phorge_active.yml
@@ -0,0 +1,21 @@
+---
+key_path: counts.groups_inheriting_phorge_active
+description: Count of active groups inheriting integrations for Phorge
+product_section: dev
+product_stage: manage
+product_group: integrations
+value_type: number
+status: active
+milestone: "16.11"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/145863
+time_frame: all
+data_source: database
+data_category: optional
+performance_indicator_type: []
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
diff --git a/doc/.vale/gitlab/spelling-exceptions.txt b/doc/.vale/gitlab/spelling-exceptions.txt
index 3543bdd77b27c..34acfe0c713be 100644
--- a/doc/.vale/gitlab/spelling-exceptions.txt
+++ b/doc/.vale/gitlab/spelling-exceptions.txt
@@ -561,6 +561,7 @@ Mailroom
 Makefile
 Makefiles
 malloc
+Maniphest
 Markdown
 markdownlint
 Marketo
@@ -687,6 +688,7 @@ Phabricator
 phaser
 phasers
 phpenv
+Phorge
 PHPUnit
 PIDs
 pipenv
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 7f29adac9f09f..92188c9d64300 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -33145,6 +33145,7 @@ State of a Sentry error.
 | <a id="servicetypemattermost_slash_commands_service"></a>`MATTERMOST_SLASH_COMMANDS_SERVICE` | MattermostSlashCommandsService type. |
 | <a id="servicetypemicrosoft_teams_service"></a>`MICROSOFT_TEAMS_SERVICE` | MicrosoftTeamsService type. |
 | <a id="servicetypepackagist_service"></a>`PACKAGIST_SERVICE` | PackagistService type. |
+| <a id="servicetypephorge_service"></a>`PHORGE_SERVICE` | PhorgeService type. |
 | <a id="servicetypepipelines_email_service"></a>`PIPELINES_EMAIL_SERVICE` | PipelinesEmailService type. |
 | <a id="servicetypepivotaltracker_service"></a>`PIVOTALTRACKER_SERVICE` | PivotaltrackerService type. |
 | <a id="servicetypeprometheus_service"></a>`PROMETHEUS_SERVICE` | PrometheusService type. |
diff --git a/doc/api/integrations.md b/doc/api/integrations.md
index 338c689fffbc8..c1fb27b2545d0 100644
--- a/doc/api/integrations.md
+++ b/doc/api/integrations.md
@@ -1417,6 +1417,41 @@ Get the Packagist integration settings for a project.
 GET /projects/:id/integrations/packagist
 ```
 
+## Phorge
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/145863) in GitLab 16.11.
+
+### Set up Phorge
+
+Set up the Phorge integration for a project.
+
+```plaintext
+PUT /projects/:id/integrations/phorge
+```
+
+Parameters:
+
+| Parameter       | Type   | Required | Description           |
+|-----------------|--------|----------|-----------------------|
+| `issues_url`    | string | true     | URL of the issue.     |
+| `project_url`   | string | true     | URL of the project.   |
+
+### Disable Phorge
+
+Disable the Phorge integration for a project. Integration settings are reset.
+
+```plaintext
+DELETE /projects/:id/integrations/phorge
+```
+
+### Get Phorge settings
+
+Get the Phorge integration settings for a project.
+
+```plaintext
+GET /projects/:id/integrations/phorge
+```
+
 ## Pipeline status emails
 
 ### Set up pipeline status emails
diff --git a/doc/integration/external-issue-tracker.md b/doc/integration/external-issue-tracker.md
index c624597e00ae2..a8f5391038fba 100644
--- a/doc/integration/external-issue-tracker.md
+++ b/doc/integration/external-issue-tracker.md
@@ -46,5 +46,6 @@ You can configure any of the following external issue trackers:
 - [Custom issue tracker](../user/project/integrations/custom_issue_tracker.md)
 - [Engineering Workflow Management (EWM)](../user/project/integrations/ewm.md)
 - [Jira](../integration/jira/index.md)
+- [Phorge](../user/project/integrations/phorge.md)
 - [Redmine](../user/project/integrations/redmine.md)
 - [YouTrack](../user/project/integrations/youtrack.md)
diff --git a/doc/user/project/integrations/index.md b/doc/user/project/integrations/index.md
index ef6cb2a94ab36..e88cc36cc9fdc 100644
--- a/doc/user/project/integrations/index.md
+++ b/doc/user/project/integrations/index.md
@@ -145,6 +145,7 @@ To use custom settings for a project or group integration:
 | [Mattermost slash commands](mattermost_slash_commands.md)                   | Run slash commands from a Mattermost chat environment.                | **{dotted-circle}** No |
 | [Microsoft Teams notifications](microsoft_teams.md)                         | Receive event notifications in Microsoft Teams.                       | **{dotted-circle}** No |
 | Packagist                                                                   | Update your PHP dependencies in Packagist.                            | **{check-circle}** Yes |
+| [Phorge](phorge.md)                                                         | Use Phorge as an issue tracker.                                       | **{dotted-circle}** No |
 | [Pipeline status emails](pipeline_status_emails.md)                         | Send the pipeline status to a list of recipients by email.            | **{dotted-circle}** No |
 | [Pivotal Tracker](pivotal_tracker.md)                                       | Add commit messages as comments to Pivotal Tracker stories.           | **{dotted-circle}** No |
 | [Pumble](pumble.md)                                                         | Send event notifications to a Pumble channel.                         | **{dotted-circle}** No |
diff --git a/doc/user/project/integrations/phorge.md b/doc/user/project/integrations/phorge.md
new file mode 100644
index 0000000000000..670f486e08b8f
--- /dev/null
+++ b/doc/user/project/integrations/phorge.md
@@ -0,0 +1,35 @@
+---
+stage: Manage
+group: Import and Integrate
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
+---
+
+# Phorge
+
+DETAILS:
+**Tier:** Free, Premium, Ultimate
+**Offering:** GitLab.com, Self-managed, GitLab Dedicated
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/145863) in GitLab 16.11.
+
+You can use [Phorge](https://we.phorge.it/) as an external issue tracker in GitLab.
+
+## Configure the integration
+
+To configure Phorge in a GitLab project:
+
+1. On the left sidebar, select **Search or go to** and find your project.
+1. Select **Settings > Integrations**.
+1. Select **Phorge**.
+1. Under **Enable integration**, select the **Active** checkbox.
+1. In **Project URL**, enter the URL to the Phorge project.
+1. In **Issue URL**, enter the URL to the Phorge project issue.
+   The URL must contain `:id`. GitLab replaces this token with the Maniphest task ID (for example, `T123`).
+1. In **New issue URL**, enter the URL to a new Phorge project issue.
+   To prefill tags related to this project, you can use `?tags=`.
+1. Optional. Select **Test settings**.
+1. Select **Save changes**.
+
+In that GitLab project, you can see a link to your Phorge project.
+You can now reference your Phorge issues and tasks in GitLab with
+`T<ID>`, where `<ID>` is a Maniphest task ID (for example, `T123`).
diff --git a/lib/api/helpers/integrations_helpers.rb b/lib/api/helpers/integrations_helpers.rb
index 769fb49af47d3..d7f1687a71b78 100644
--- a/lib/api/helpers/integrations_helpers.rb
+++ b/lib/api/helpers/integrations_helpers.rb
@@ -397,6 +397,7 @@ def self.integrations
               desc: 'The server'
             }
           ],
+          'phorge' => ::Integrations::Phorge.api_fields,
           'pipelines-email' => [
             {
               required: true,
@@ -658,6 +659,7 @@ def self.integration_classes
           ::Integrations::MattermostSlashCommands,
           ::Integrations::MicrosoftTeams,
           ::Integrations::Packagist,
+          ::Integrations::Phorge,
           ::Integrations::PipelinesEmail,
           ::Integrations::Pivotaltracker,
           ::Integrations::Prometheus,
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 2711e54096aa1..f7391410ad60b 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -27973,6 +27973,12 @@ msgstr ""
 msgid "IssueTracker|Use JetBrains YouTrack as this project's issue tracker. %{docs_link}"
 msgstr ""
 
+msgid "IssueTracker|Use Phorge as this project's issue tracker."
+msgstr ""
+
+msgid "IssueTracker|Use Phorge as this project's issue tracker. %{docs_link}"
+msgstr ""
+
 msgid "IssueTracker|Use Redmine as the issue tracker. %{docs_link}"
 msgstr ""
 
diff --git a/spec/factories/integrations.rb b/spec/factories/integrations.rb
index 5978755e5f5d4..a7cb82cc886ae 100644
--- a/spec/factories/integrations.rb
+++ b/spec/factories/integrations.rb
@@ -82,6 +82,12 @@
     server { 'https://packagist.example.comp' }
   end
 
+  factory :phorge_integration, class: 'Integrations::Phorge' do
+    project
+    active { true }
+    issue_tracker
+  end
+
   factory :prometheus_integration, class: 'Integrations::Prometheus' do
     project
     active { true }
diff --git a/spec/features/projects/integrations/user_activates_issue_tracker_spec.rb b/spec/features/projects/integrations/user_activates_issue_tracker_spec.rb
index 02cec948127bf..693776b612c2c 100644
--- a/spec/features/projects/integrations/user_activates_issue_tracker_spec.rb
+++ b/spec/features/projects/integrations/user_activates_issue_tracker_spec.rb
@@ -92,4 +92,5 @@ def fill_form(disable: false, skip_new_issue_url: false)
   it_behaves_like 'external issue tracker activation', tracker: 'Custom issue tracker'
   it_behaves_like 'external issue tracker activation', tracker: 'EWM', skip_test: true
   it_behaves_like 'external issue tracker activation', tracker: 'ClickUp', skip_new_issue_url: true
+  it_behaves_like 'external issue tracker activation', tracker: 'Phorge'
 end
diff --git a/spec/graphql/types/projects/service_type_enum_spec.rb b/spec/graphql/types/projects/service_type_enum_spec.rb
index 40376afc7f78f..36c00212579d2 100644
--- a/spec/graphql/types/projects/service_type_enum_spec.rb
+++ b/spec/graphql/types/projects/service_type_enum_spec.rb
@@ -32,6 +32,7 @@ def core_service_enums
       MATTERMOST_SLASH_COMMANDS_SERVICE
       MICROSOFT_TEAMS_SERVICE
       PACKAGIST_SERVICE
+      PHORGE_SERVICE
       PIPELINES_EMAIL_SERVICE
       PIVOTALTRACKER_SERVICE
       PROMETHEUS_SERVICE
diff --git a/spec/lib/banzai/filter/references/external_issue_reference_filter_spec.rb b/spec/lib/banzai/filter/references/external_issue_reference_filter_spec.rb
index acc59c85cbf85..9ff298c32d29a 100644
--- a/spec/lib/banzai/filter/references/external_issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/external_issue_reference_filter_spec.rb
@@ -325,6 +325,23 @@
     end
   end
 
+  context "phorge project" do
+    before_all do
+      create(:phorge_integration, project: project)
+    end
+
+    before do
+      project.update!(issues_enabled: false)
+    end
+
+    context "with right markdown" do
+      let(:issue) { ExternalIssue.new("T123", project) }
+      let(:reference) { issue.to_reference }
+
+      it_behaves_like "external issue tracker"
+    end
+  end
+
   context 'checking N+1' do
     let_it_be(:integration) { create(:redmine_integration, project: project) }
     let_it_be(:issue1) { ExternalIssue.new("#123", project) }
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 69f32b4145f80..620980217d532 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -587,6 +587,7 @@ project:
 - harbor_integration
 - irker_integration
 - packagist_integration
+- phorge_integration
 - pivotaltracker_integration
 - prometheus_integration
 - assembla_integration
diff --git a/spec/models/integrations/phorge_spec.rb b/spec/models/integrations/phorge_spec.rb
new file mode 100644
index 0000000000000..895578ed9a8f7
--- /dev/null
+++ b/spec/models/integrations/phorge_spec.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::Phorge, feature_category: :integrations do
+  describe 'Validations' do
+    subject { build(:phorge_integration, active: active) }
+
+    context 'when integration is active' do
+      let(:active) { true }
+
+      it { is_expected.to validate_presence_of(:project_url) }
+      it { is_expected.to validate_presence_of(:issues_url) }
+      it { is_expected.to validate_presence_of(:new_issue_url) }
+
+      it_behaves_like 'issue tracker integration URL attribute', :project_url
+      it_behaves_like 'issue tracker integration URL attribute', :issues_url
+      it_behaves_like 'issue tracker integration URL attribute', :new_issue_url
+    end
+
+    context 'when integration is inactive' do
+      let(:active) { false }
+
+      it { is_expected.not_to validate_presence_of(:project_url) }
+      it { is_expected.not_to validate_presence_of(:issues_url) }
+      it { is_expected.not_to validate_presence_of(:new_issue_url) }
+    end
+  end
+
+  describe '#reference_pattern' do
+    using RSpec::Parameterized::TableSyntax
+
+    let(:reference_pattern) { build(:phorge_integration).reference_pattern }
+
+    subject { reference_pattern }
+
+    context 'when text contains a Phorge Maniphest task reference' do
+      where(:text, :reference) do
+        'Referencing T111' | 'T111'
+        'Referencing T222, mid sentence' | 'T222'
+        'Referencing (T333) in parentheses' | 'T333'
+        'Referencing #T444 with a hash prefix' | 'T444'
+      end
+
+      with_them do
+        it { is_expected.to match(text) }
+
+        it 'captures the task reference' do
+          expect(reference_pattern.match(text)[:issue]).to eq(reference)
+        end
+      end
+    end
+
+    context 'when text contains something resembling but is not a Phorge Maniphest task reference' do
+      where(:text) do
+        [
+          'See docs for Model-T1',
+          'cc user @T1'
+        ]
+      end
+
+      with_them do
+        it { is_expected.not_to match(text) }
+      end
+    end
+  end
+end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 6366e5c986322..c4f98d6581f8d 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -66,6 +66,7 @@
     it { is_expected.to have_one(:pumble_integration) }
     it { is_expected.to have_one(:webex_teams_integration) }
     it { is_expected.to have_one(:packagist_integration) }
+    it { is_expected.to have_one(:phorge_integration) }
     it { is_expected.to have_one(:pushover_integration) }
     it { is_expected.to have_one(:apple_app_store_integration) }
     it { is_expected.to have_one(:google_play_integration) }
-- 
GitLab