From c15d7abadff7382d34457059c233a3d1fcfe1be5 Mon Sep 17 00:00:00 2001
From: Shinya Maeda <shinya@gitlab.com>
Date: Wed, 8 Sep 2021 10:55:44 +0700
Subject: [PATCH] Validate accidental override by presenters

This commit validates the accidental override by presenters.
---
 app/presenters/README.md                      | 95 ++++++++++++++++--
 .../alert_management/alert_presenter.rb       |  4 +
 app/presenters/award_emoji_presenter.rb       |  2 +-
 app/presenters/blob_presenter.rb              |  2 +-
 app/presenters/blobs/unfold_presenter.rb      |  2 +
 app/presenters/board_presenter.rb             |  2 +-
 app/presenters/ci/bridge_presenter.rb         |  3 +
 app/presenters/ci/build_metadata_presenter.rb |  3 +-
 app/presenters/ci/build_presenter.rb          |  2 +
 app/presenters/ci/group_variable_presenter.rb |  2 +-
 app/presenters/ci/legacy_stage_presenter.rb   |  2 +-
 app/presenters/ci/pipeline_presenter.rb       |  7 +-
 app/presenters/ci/processable_presenter.rb    |  1 +
 app/presenters/ci/runner_presenter.rb         |  5 +-
 app/presenters/ci/stage_presenter.rb          |  2 +-
 app/presenters/ci/trigger_presenter.rb        |  3 +-
 app/presenters/ci/variable_presenter.rb       |  2 +-
 app/presenters/clusterable_presenter.rb       |  2 +-
 app/presenters/clusters/cluster_presenter.rb  |  4 +-
 .../clusters/integration_presenter.rb         |  2 +-
 app/presenters/commit_presenter.rb            |  2 +-
 app/presenters/commit_status_presenter.rb     |  2 +-
 .../dev_ops_report/metric_presenter.rb        |  2 +
 app/presenters/environment_presenter.rb       |  2 +-
 app/presenters/event_presenter.rb             |  3 +-
 app/presenters/gitlab/blame_presenter.rb      |  2 +-
 app/presenters/group_clusterable_presenter.rb |  2 +
 app/presenters/group_member_presenter.rb      |  2 +
 .../instance_clusterable_presenter.rb         |  2 +
 app/presenters/invitation_presenter.rb        |  2 +-
 app/presenters/issue_presenter.rb             |  3 +-
 app/presenters/label_presenter.rb             |  4 +-
 app/presenters/member_presenter.rb            |  2 +-
 app/presenters/members_presenter.rb           |  2 +-
 app/presenters/merge_request_presenter.rb     |  6 +-
 app/presenters/milestone_presenter.rb         |  2 +-
 app/presenters/pages_domain_presenter.rb      |  4 +-
 .../project_clusterable_presenter.rb          |  2 +
 app/presenters/project_hook_presenter.rb      |  2 +-
 app/presenters/project_member_presenter.rb    |  2 +
 app/presenters/project_presenter.rb           |  5 +-
 .../import_export/project_export_presenter.rb | 10 +-
 .../settings/deploy_keys_presenter.rb         |  2 +-
 app/presenters/prometheus_alert_presenter.rb  |  2 +-
 app/presenters/release_presenter.rb           |  6 +-
 app/presenters/releases/evidence_presenter.rb |  2 +-
 app/presenters/search_service_presenter.rb    |  3 +-
 app/presenters/sentry_error_presenter.rb      |  2 +-
 app/presenters/service_hook_presenter.rb      |  2 +-
 app/presenters/snippet_blob_presenter.rb      |  2 +
 app/presenters/snippet_presenter.rb           |  5 +-
 app/presenters/terraform/modules_presenter.rb |  2 +-
 app/presenters/todo_presenter.rb              |  2 +-
 app/presenters/tree_entry_presenter.rb        |  2 +-
 app/presenters/user_presenter.rb              |  2 +-
 app/presenters/web_hook_log_presenter.rb      |  2 +-
 doc/development/reusing_abstractions.md       |  2 +
 ee/app/presenters/audit_event_presenter.rb    |  2 +-
 .../presenters/boards/epic_board_presenter.rb |  2 +-
 .../presenters/dast/site_profile_presenter.rb |  2 +-
 ee/app/presenters/ee/ci/build_presenter.rb    |  2 +
 ee/app/presenters/ee/ci/pipeline_presenter.rb |  2 +
 ee/app/presenters/ee/label_presenter.rb       |  3 +
 .../presenters/ee/merge_request_presenter.rb  |  6 ++
 ee/app/presenters/ee/project_presenter.rb     |  2 +
 ee/app/presenters/epic_issue_presenter.rb     |  3 +-
 ee/app/presenters/epic_presenter.rb           |  3 +-
 ee/app/presenters/iteration_presenter.rb      |  2 +-
 .../merge_request_approver_presenter.rb       |  2 +-
 .../security/configuration_presenter.rb       |  4 +-
 ee/app/presenters/security/scan_presenter.rb  |  3 +-
 ee/app/presenters/subscription_presenter.rb   |  2 +-
 .../subscriptions/new_plan_presenter.rb       |  3 +
 .../vulnerabilities/finding_presenter.rb      |  2 +-
 ee/app/presenters/vulnerability_presenter.rb  |  6 +-
 lib/gitlab/utils/delegator_override.rb        | 48 +++++++++
 lib/gitlab/utils/delegator_override/error.rb  | 23 +++++
 .../utils/delegator_override/validator.rb     | 99 +++++++++++++++++++
 lib/gitlab/view/presenter/base.rb             | 14 ++-
 lib/gitlab/view/presenter/delegated.rb        | 11 +++
 lib/tasks/lint.rake                           |  1 +
 .../utils/delegator_override/error_spec.rb    | 13 +++
 .../delegator_override/validator_spec.rb      | 81 +++++++++++++++
 .../gitlab/utils/delegator_override_spec.rb   | 97 ++++++++++++++++++
 spec/lib/gitlab/view/presenter/base_spec.rb   | 34 ++++++-
 85 files changed, 657 insertions(+), 65 deletions(-)
 create mode 100644 lib/gitlab/utils/delegator_override.rb
 create mode 100644 lib/gitlab/utils/delegator_override/error.rb
 create mode 100644 lib/gitlab/utils/delegator_override/validator.rb
 create mode 100644 spec/lib/gitlab/utils/delegator_override/error_spec.rb
 create mode 100644 spec/lib/gitlab/utils/delegator_override/validator_spec.rb
 create mode 100644 spec/lib/gitlab/utils/delegator_override_spec.rb

diff --git a/app/presenters/README.md b/app/presenters/README.md
index 62aec4fc8a20..dfd1818f97dd 100644
--- a/app/presenters/README.md
+++ b/app/presenters/README.md
@@ -66,14 +66,15 @@ we gain the following benefits:
 
 ### Presenter definition
 
-Every presenter should inherit from `Gitlab::View::Presenter::Simple`, which
-provides a `.presents` the method which allows you to define an accessor for the
+If you need a presenter class that has only necessary interfaces for the view-related context,
+inherit from `Gitlab::View::Presenter::Simple`.
+It provides a `.presents` the method which allows you to define an accessor for the
 presented object. It also includes common helpers like `Gitlab::Routing` and
 `Gitlab::Allowable`.
 
 ```ruby
 class LabelPresenter < Gitlab::View::Presenter::Simple
-  presents :label
+  presents ::Label, as: :label
 
   def text_color
     label.color.to_s
@@ -85,13 +86,14 @@ class LabelPresenter < Gitlab::View::Presenter::Simple
 end
 ```
 
-In some cases, it can be more practical to transparently delegate all missing
-method calls to the presented object, in these cases, you can make your
-presenter inherit from `Gitlab::View::Presenter::Delegated`:
+If you need a presenter class that delegates missing method calls to the presented object,
+inherit from `Gitlab::View::Presenter::Delegated`.
+This is more like an "extension" in the sense that the produced object is going to have
+all of interfaces of the presented object **AND** all of the interfaces in the presenter class:
 
 ```ruby
 class LabelPresenter < Gitlab::View::Presenter::Delegated
-  presents :label
+  presents ::Label, as: :label
 
   def text_color
     # color is delegated to label
@@ -152,3 +154,82 @@ You can also present the model in the view:
 %div{ class: label.text_color }
   = render partial: label, label: label
 ```
+
+### Validate accidental overrides
+
+We use presenters in many places, such as Controller, Haml, GraphQL/Rest API,
+it's very handy to extend the core/backend logic of Active Record models,
+however, there is a risk that it accidentally overrides important logic.
+
+For example, [this production incident](https://gitlab.com/gitlab-com/gl-infra/production/-/issues/5498)
+was caused by [including `ActionView::Helpers::UrlHelper` in a presenter](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69537/diffs#4b581cff00ef3cc9780efd23682af383de302e7d_3_3).
+The `tag` accesor in `Ci::Build` was accidentally overridden by `ActionView::Helpers::TagHelper#tag`,
+and as a conseuqence, a wrong `tag` value was persited into database.
+
+Starting from GitLab 14.4, we validate the presenters (specifically all of the subclasses of `Gitlab::View::Presenter::Delegated`)
+that they do not accidentally override core/backend logic. In such case, a pipeline in merge requests fails with an error message,
+here is an example:
+
+```plaintext
+We've detected that a presetner is overriding a specific method(s) on a subject model.
+There is a risk that it accidentally modifies the backend/core logic that leads to production incident.
+Please follow https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/presenters/README.md#validate-accidental-overrides
+to resolve this error with caution.
+
+Here are the conflict details.
+
+- Ci::PipelinePresenter#tag is overriding Ci::Pipeline#tag. delegator_location: /devkitkat/services/rails/cache/ruby/2.7.0/gems/actionview-6.1.3.2/lib/action_view/helpers/tag_helper.rb:271 original_location: /devkitkat/services/rails/cache/ruby/2.7.0/gems/activemodel-6.1.3.2/lib/active_model/attribute_methods.rb:254
+```
+
+Here are the potential solutions:
+
+- If the conflict happens on an instance method in the presenter:
+  - If you intend to override the core/backend logic, define `delegator_override <method-name>` on top of the conflicted method.
+    This explicitly adds the method to an allowlist.
+  - If you do NOT intend to override the core/backend logic, rename the method name in the presenter.
+- If the conflict happens on an included module in the presenter, remove the module from the presenter and find a workaround.
+
+### How to use the `Gitlab::Utils::DelegatorOverride` validator
+
+If a presenter class inhertis from `Gitlab::View::Presenter::Delegated`,
+you should define what object class is presented:
+
+```ruby
+class WebHookLogPresenter < Gitlab::View::Presenter::Delegated
+  presents ::WebHookLog, as: :web_hook_log            # This defines that the presenter presents `WebHookLog` Active Record model.
+```
+
+These presenters are validated not to accidentaly override the methods in the presented object.
+You can run the validation locally with:
+
+```shell
+bundle exec rake lint:static_verification
+```
+
+To add a method to an allowlist, use `delegator_override`. For example:
+
+```ruby
+class VulnerabilityPresenter < Gitlab::View::Presenter::Delegated
+  presents ::Vulnerability, as: :vulnerability
+
+  delegator_override :description                 # This adds the `description` method to an allowlist that the override is intentional.
+  def description
+    vulnerability.description || finding.description
+  end
+```
+
+To add methods of a module to an allowlist, use `delegator_override_with`. For example:
+
+```ruby
+module Ci
+  class PipelinePresenter < Gitlab::View::Presenter::Delegated
+    include Gitlab::Utils::StrongMemoize
+    include ActionView::Helpers::UrlHelper
+
+    delegator_override_with Gitlab::Utils::StrongMemoize # TODO: Remove `Gitlab::Utils::StrongMemoize` inclusion as it's duplicate
+    delegator_override_with ActionView::Helpers::TagHelper # TODO: Remove `ActionView::Helpers::UrlHelper` inclusion as it overrides `Ci::Pipeline#tag`
+```
+
+Keep in mind that if you use `delegator_override_with`,
+there is a high chance that you're doing **something wrong**.
+Read the [Validate Accidental Overrides](#validate-accidental-overrides) for more information.
diff --git a/app/presenters/alert_management/alert_presenter.rb b/app/presenters/alert_management/alert_presenter.rb
index 93983c989c89..86fe98592716 100644
--- a/app/presenters/alert_management/alert_presenter.rb
+++ b/app/presenters/alert_management/alert_presenter.rb
@@ -5,6 +5,9 @@ class AlertPresenter < Gitlab::View::Presenter::Delegated
     include IncidentManagement::Settings
     include ActionView::Helpers::UrlHelper
 
+    presents ::AlertManagement::Alert
+    delegator_override_with Gitlab::Utils::StrongMemoize # TODO: Remove `Gitlab::Utils::StrongMemoize` inclusion as it's duplicate
+
     MARKDOWN_LINE_BREAK = "  \n"
     HORIZONTAL_LINE = "\n\n---\n\n"
 
@@ -29,6 +32,7 @@ def start_time
       started_at&.strftime('%d %B %Y, %-l:%M%p (%Z)')
     end
 
+    delegator_override :details_url
     def details_url
       details_project_alert_management_url(project, alert.iid)
     end
diff --git a/app/presenters/award_emoji_presenter.rb b/app/presenters/award_emoji_presenter.rb
index 98713855d35d..8a7b58e0aba2 100644
--- a/app/presenters/award_emoji_presenter.rb
+++ b/app/presenters/award_emoji_presenter.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class AwardEmojiPresenter < Gitlab::View::Presenter::Delegated
-  presents :award_emoji
+  presents ::AwardEmoji, as: :award_emoji
 
   def description
     as_emoji['description']
diff --git a/app/presenters/blob_presenter.rb b/app/presenters/blob_presenter.rb
index ecc16e2840c3..c198859aa4c8 100644
--- a/app/presenters/blob_presenter.rb
+++ b/app/presenters/blob_presenter.rb
@@ -7,7 +7,7 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated
   include TreeHelper
   include ChecksCollaboration
 
-  presents :blob
+  presents ::Blob, as: :blob
 
   def highlight(to: nil, plain: nil)
     load_all_blob_data
diff --git a/app/presenters/blobs/unfold_presenter.rb b/app/presenters/blobs/unfold_presenter.rb
index 487c6fe07577..b921b5bf670a 100644
--- a/app/presenters/blobs/unfold_presenter.rb
+++ b/app/presenters/blobs/unfold_presenter.rb
@@ -6,6 +6,8 @@ class UnfoldPresenter < BlobPresenter
     include ActiveModel::AttributeAssignment
     include Gitlab::Utils::StrongMemoize
 
+    presents ::Blob
+
     attribute :full, :boolean, default: false
     attribute :since, :integer, default: 1
     attribute :to, :integer, default: 1
diff --git a/app/presenters/board_presenter.rb b/app/presenters/board_presenter.rb
index d7cecd44dd7f..bb3e96b8fafc 100644
--- a/app/presenters/board_presenter.rb
+++ b/app/presenters/board_presenter.rb
@@ -1,5 +1,5 @@
 # frozen_string_literal: true
 
 class BoardPresenter < Gitlab::View::Presenter::Delegated
-  presents :board
+  presents ::Board, as: :board
 end
diff --git a/app/presenters/ci/bridge_presenter.rb b/app/presenters/ci/bridge_presenter.rb
index 724e10c26c33..a62d7cdbbd43 100644
--- a/app/presenters/ci/bridge_presenter.rb
+++ b/app/presenters/ci/bridge_presenter.rb
@@ -2,6 +2,9 @@
 
 module Ci
   class BridgePresenter < ProcessablePresenter
+    presents ::Ci::Bridge
+
+    delegator_override :detailed_status
     def detailed_status
       @detailed_status ||= subject.detailed_status(user)
     end
diff --git a/app/presenters/ci/build_metadata_presenter.rb b/app/presenters/ci/build_metadata_presenter.rb
index 4871bb3a9199..2f559adf77dc 100644
--- a/app/presenters/ci/build_metadata_presenter.rb
+++ b/app/presenters/ci/build_metadata_presenter.rb
@@ -9,8 +9,9 @@ class BuildMetadataPresenter < Gitlab::View::Presenter::Delegated
         job_timeout_source: 'job'
     }.freeze
 
-    presents :metadata
+    presents ::Ci::BuildMetadata, as: :metadata
 
+    delegator_override :timeout_source
     def timeout_source
       return unless metadata.timeout_source?
 
diff --git a/app/presenters/ci/build_presenter.rb b/app/presenters/ci/build_presenter.rb
index 06ed6791bb74..65e1c80085f5 100644
--- a/app/presenters/ci/build_presenter.rb
+++ b/app/presenters/ci/build_presenter.rb
@@ -2,6 +2,8 @@
 
 module Ci
   class BuildPresenter < ProcessablePresenter
+    presents ::Ci::Build
+
     def erased_by_user?
       # Build can be erased through API, therefore it does not have
       # `erased_by` user assigned in that case.
diff --git a/app/presenters/ci/group_variable_presenter.rb b/app/presenters/ci/group_variable_presenter.rb
index 99011150c84e..dea9a42b6224 100644
--- a/app/presenters/ci/group_variable_presenter.rb
+++ b/app/presenters/ci/group_variable_presenter.rb
@@ -2,7 +2,7 @@
 
 module Ci
   class GroupVariablePresenter < Gitlab::View::Presenter::Delegated
-    presents :variable
+    presents ::Ci::GroupVariable, as: :variable
 
     def placeholder
       'GROUP_VARIABLE'
diff --git a/app/presenters/ci/legacy_stage_presenter.rb b/app/presenters/ci/legacy_stage_presenter.rb
index d5c21baba287..c803abfab6a1 100644
--- a/app/presenters/ci/legacy_stage_presenter.rb
+++ b/app/presenters/ci/legacy_stage_presenter.rb
@@ -2,7 +2,7 @@
 
 module Ci
   class LegacyStagePresenter < Gitlab::View::Presenter::Delegated
-    presents :legacy_stage
+    presents ::Ci::LegacyStage, as: :legacy_stage
 
     def latest_ordered_statuses
       preload_statuses(legacy_stage.statuses.latest_ordered)
diff --git a/app/presenters/ci/pipeline_presenter.rb b/app/presenters/ci/pipeline_presenter.rb
index 82f00f746924..e0cb899c9d39 100644
--- a/app/presenters/ci/pipeline_presenter.rb
+++ b/app/presenters/ci/pipeline_presenter.rb
@@ -5,6 +5,9 @@ class PipelinePresenter < Gitlab::View::Presenter::Delegated
     include Gitlab::Utils::StrongMemoize
     include ActionView::Helpers::UrlHelper
 
+    delegator_override_with Gitlab::Utils::StrongMemoize # TODO: Remove `Gitlab::Utils::StrongMemoize` inclusion as it's duplicate
+    delegator_override_with ActionView::Helpers::TagHelper # TODO: Remove `ActionView::Helpers::UrlHelper` inclusion as it overrides `Ci::Pipeline#tag`
+
     # We use a class method here instead of a constant, allowing EE to redefine
     # the returned `Hash` more easily.
     def self.failure_reasons
@@ -20,8 +23,9 @@ def self.failure_reasons
         user_blocked: 'The user who created this pipeline is blocked.' }
     end
 
-    presents :pipeline
+    presents ::Ci::Pipeline, as: :pipeline
 
+    delegator_override :failed_builds
     def failed_builds
       return [] unless can?(current_user, :read_build, pipeline)
 
@@ -30,6 +34,7 @@ def failed_builds
       end
     end
 
+    delegator_override :failure_reason
     def failure_reason
       return unless pipeline.failure_reason?
 
diff --git a/app/presenters/ci/processable_presenter.rb b/app/presenters/ci/processable_presenter.rb
index 5a8a66490711..9f3a83a00f3e 100644
--- a/app/presenters/ci/processable_presenter.rb
+++ b/app/presenters/ci/processable_presenter.rb
@@ -2,5 +2,6 @@
 
 module Ci
   class ProcessablePresenter < CommitStatusPresenter
+    presents ::Ci::Processable
   end
 end
diff --git a/app/presenters/ci/runner_presenter.rb b/app/presenters/ci/runner_presenter.rb
index 273328afc53f..ad889d444f86 100644
--- a/app/presenters/ci/runner_presenter.rb
+++ b/app/presenters/ci/runner_presenter.rb
@@ -2,11 +2,14 @@
 
 module Ci
   class RunnerPresenter < Gitlab::View::Presenter::Delegated
-    presents :runner
+    presents ::Ci::Runner, as: :runner
 
+    delegator_override :locked?
     def locked?
       read_attribute(:locked) && project_type?
     end
+
+    delegator_override :locked
     alias_method :locked, :locked?
   end
 end
diff --git a/app/presenters/ci/stage_presenter.rb b/app/presenters/ci/stage_presenter.rb
index 21bda86cdeda..bd5bf08abbca 100644
--- a/app/presenters/ci/stage_presenter.rb
+++ b/app/presenters/ci/stage_presenter.rb
@@ -2,7 +2,7 @@
 
 module Ci
   class StagePresenter < Gitlab::View::Presenter::Delegated
-    presents :stage
+    presents ::Ci::Stage, as: :stage
 
     PRELOADED_RELATIONS = [:pipeline, :metadata, :tags, :job_artifacts_archive, :downstream_pipeline].freeze
 
diff --git a/app/presenters/ci/trigger_presenter.rb b/app/presenters/ci/trigger_presenter.rb
index 605c8f328a47..5f0bd4b3a8bb 100644
--- a/app/presenters/ci/trigger_presenter.rb
+++ b/app/presenters/ci/trigger_presenter.rb
@@ -2,12 +2,13 @@
 
 module Ci
   class TriggerPresenter < Gitlab::View::Presenter::Delegated
-    presents :trigger
+    presents ::Ci::Trigger, as: :trigger
 
     def has_token_exposed?
       can?(current_user, :admin_trigger, trigger)
     end
 
+    delegator_override :token
     def token
       if has_token_exposed?
         trigger.token
diff --git a/app/presenters/ci/variable_presenter.rb b/app/presenters/ci/variable_presenter.rb
index f027f3aa5600..ec04dd5e9ff0 100644
--- a/app/presenters/ci/variable_presenter.rb
+++ b/app/presenters/ci/variable_presenter.rb
@@ -2,7 +2,7 @@
 
 module Ci
   class VariablePresenter < Gitlab::View::Presenter::Delegated
-    presents :variable
+    presents ::Ci::Variable, as: :variable
 
     def placeholder
       'PROJECT_VARIABLE'
diff --git a/app/presenters/clusterable_presenter.rb b/app/presenters/clusterable_presenter.rb
index 03e26b929229..4b645510b515 100644
--- a/app/presenters/clusterable_presenter.rb
+++ b/app/presenters/clusterable_presenter.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class ClusterablePresenter < Gitlab::View::Presenter::Delegated
-  presents :clusterable
+  presents ::Project, ::Group, ::Clusters::Instance, as: :clusterable
 
   def self.fabricate(clusterable, **attributes)
     presenter_class = "#{clusterable.class.name}ClusterablePresenter".constantize
diff --git a/app/presenters/clusters/cluster_presenter.rb b/app/presenters/clusters/cluster_presenter.rb
index eb4bd8532afe..892ea14267bc 100644
--- a/app/presenters/clusters/cluster_presenter.rb
+++ b/app/presenters/clusters/cluster_presenter.rb
@@ -7,7 +7,9 @@ class ClusterPresenter < Gitlab::View::Presenter::Delegated
     include ActionView::Helpers::UrlHelper
     include IconsHelper
 
-    presents :cluster
+    delegator_override_with ::Gitlab::Utils::StrongMemoize # TODO: Remove `::Gitlab::Utils::StrongMemoize` inclusion as it's duplicate
+
+    presents ::Clusters::Cluster, as: :cluster
 
     # We do not want to show the group path for clusters belonging to the
     # clusterable, only for the ancestor clusters.
diff --git a/app/presenters/clusters/integration_presenter.rb b/app/presenters/clusters/integration_presenter.rb
index 57608be29b15..f7be59f00f3c 100644
--- a/app/presenters/clusters/integration_presenter.rb
+++ b/app/presenters/clusters/integration_presenter.rb
@@ -2,7 +2,7 @@
 
 module Clusters
   class IntegrationPresenter < Gitlab::View::Presenter::Delegated
-    presents :integration
+    presents ::Clusters::Integrations::Prometheus, ::Clusters::Integrations::ElasticStack, as: :integration
 
     def application_type
       integration.class.name.demodulize.underscore
diff --git a/app/presenters/commit_presenter.rb b/app/presenters/commit_presenter.rb
index c14dcab60007..7df45ca03bba 100644
--- a/app/presenters/commit_presenter.rb
+++ b/app/presenters/commit_presenter.rb
@@ -3,7 +3,7 @@
 class CommitPresenter < Gitlab::View::Presenter::Delegated
   include GlobalID::Identification
 
-  presents :commit
+  presents ::Commit, as: :commit
 
   def status_for(ref)
     return unless can?(current_user, :read_commit_status, commit.project)
diff --git a/app/presenters/commit_status_presenter.rb b/app/presenters/commit_status_presenter.rb
index 3c39470b730c..2d524fd611ec 100644
--- a/app/presenters/commit_status_presenter.rb
+++ b/app/presenters/commit_status_presenter.rb
@@ -33,7 +33,7 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
 
   private_constant :CALLOUT_FAILURE_MESSAGES
 
-  presents :build
+  presents ::CommitStatus, as: :build
 
   def self.callout_failure_messages
     CALLOUT_FAILURE_MESSAGES
diff --git a/app/presenters/dev_ops_report/metric_presenter.rb b/app/presenters/dev_ops_report/metric_presenter.rb
index 4d7ac1cd3ec9..55326f8f678f 100644
--- a/app/presenters/dev_ops_report/metric_presenter.rb
+++ b/app/presenters/dev_ops_report/metric_presenter.rb
@@ -2,6 +2,8 @@
 
 module DevOpsReport
   class MetricPresenter < Gitlab::View::Presenter::Simple
+    presents ::DevOpsReport::Metric
+
     delegate :created_at, to: :subject
 
     def cards
diff --git a/app/presenters/environment_presenter.rb b/app/presenters/environment_presenter.rb
index 6f67bbe2a5a9..6c8da86187c0 100644
--- a/app/presenters/environment_presenter.rb
+++ b/app/presenters/environment_presenter.rb
@@ -3,7 +3,7 @@
 class EnvironmentPresenter < Gitlab::View::Presenter::Delegated
   include ActionView::Helpers::UrlHelper
 
-  presents :environment
+  presents ::Environment, as: :environment
 
   def path
     project_environment_path(project, self)
diff --git a/app/presenters/event_presenter.rb b/app/presenters/event_presenter.rb
index c37721f7213d..4c787b88e20f 100644
--- a/app/presenters/event_presenter.rb
+++ b/app/presenters/event_presenter.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class EventPresenter < Gitlab::View::Presenter::Delegated
-  presents :event
+  presents ::Event, as: :event
 
   def initialize(subject, **attributes)
     super
@@ -10,6 +10,7 @@ def initialize(subject, **attributes)
   end
 
   # Caching `visible_to_user?` method in the presenter beause it might be called multiple times.
+  delegator_override :visible_to_user?
   def visible_to_user?(user = nil)
     @visible_to_user_cache.fetch(user&.id) { super(user) }
   end
diff --git a/app/presenters/gitlab/blame_presenter.rb b/app/presenters/gitlab/blame_presenter.rb
index 1f2445b04a1f..e9340a42e51f 100644
--- a/app/presenters/gitlab/blame_presenter.rb
+++ b/app/presenters/gitlab/blame_presenter.rb
@@ -12,7 +12,7 @@ class BlamePresenter < Gitlab::View::Presenter::Simple
     include TreeHelper
     include IconsHelper
 
-    presents :blame
+    presents nil, as: :blame
 
     CommitData = Struct.new(
       :author_avatar,
diff --git a/app/presenters/group_clusterable_presenter.rb b/app/presenters/group_clusterable_presenter.rb
index 34e7084ab029..207767af245e 100644
--- a/app/presenters/group_clusterable_presenter.rb
+++ b/app/presenters/group_clusterable_presenter.rb
@@ -4,6 +4,8 @@ class GroupClusterablePresenter < ClusterablePresenter
   extend ::Gitlab::Utils::Override
   include ActionView::Helpers::UrlHelper
 
+  presents ::Group
+
   override :cluster_status_cluster_path
   def cluster_status_cluster_path(cluster, params = {})
     cluster_status_group_cluster_path(clusterable, cluster, params)
diff --git a/app/presenters/group_member_presenter.rb b/app/presenters/group_member_presenter.rb
index 5ab4b51f472b..88facc3608de 100644
--- a/app/presenters/group_member_presenter.rb
+++ b/app/presenters/group_member_presenter.rb
@@ -1,6 +1,8 @@
 # frozen_string_literal: true
 
 class GroupMemberPresenter < MemberPresenter
+  presents ::GroupMember
+
   private
 
   def admin_member_permission
diff --git a/app/presenters/instance_clusterable_presenter.rb b/app/presenters/instance_clusterable_presenter.rb
index 56d91f90b2ea..77ca171156d6 100644
--- a/app/presenters/instance_clusterable_presenter.rb
+++ b/app/presenters/instance_clusterable_presenter.rb
@@ -4,6 +4,8 @@ class InstanceClusterablePresenter < ClusterablePresenter
   extend ::Gitlab::Utils::Override
   include ActionView::Helpers::UrlHelper
 
+  presents ::Clusters::Instance
+
   def self.fabricate(clusterable, **attributes)
     attributes_with_presenter_class = attributes.merge(presenter_class: InstanceClusterablePresenter)
 
diff --git a/app/presenters/invitation_presenter.rb b/app/presenters/invitation_presenter.rb
index d8c07f327dd6..ada8227a4771 100644
--- a/app/presenters/invitation_presenter.rb
+++ b/app/presenters/invitation_presenter.rb
@@ -1,5 +1,5 @@
 # frozen_string_literal: true
 
 class InvitationPresenter < Gitlab::View::Presenter::Delegated
-  presents :invitation
+  presents nil, as: :invitation
 end
diff --git a/app/presenters/issue_presenter.rb b/app/presenters/issue_presenter.rb
index b7f4ac0555d4..72fe14d224de 100644
--- a/app/presenters/issue_presenter.rb
+++ b/app/presenters/issue_presenter.rb
@@ -1,12 +1,13 @@
 # frozen_string_literal: true
 
 class IssuePresenter < Gitlab::View::Presenter::Delegated
-  presents :issue
+  presents ::Issue, as: :issue
 
   def issue_path
     url_builder.build(issue, only_path: true)
   end
 
+  delegator_override :subscribed?
   def subscribed?
     issue.subscribed?(current_user, issue.project)
   end
diff --git a/app/presenters/label_presenter.rb b/app/presenters/label_presenter.rb
index 9e51e6fa4bae..fafade2828f0 100644
--- a/app/presenters/label_presenter.rb
+++ b/app/presenters/label_presenter.rb
@@ -1,9 +1,11 @@
 # frozen_string_literal: true
 
 class LabelPresenter < Gitlab::View::Presenter::Delegated
-  presents :label
+  presents ::Label, as: :label
   delegate :name, :full_name, to: :label_subject, prefix: :subject
 
+  delegator_override :subject # TODO: Fix `Gitlab::View::Presenter::Delegated#subject` not to override `Label#subject`.
+
   def edit_path
     case label
     when GroupLabel then edit_group_label_path(label.group, label)
diff --git a/app/presenters/member_presenter.rb b/app/presenters/member_presenter.rb
index b37a43bf251c..67d044dd01c3 100644
--- a/app/presenters/member_presenter.rb
+++ b/app/presenters/member_presenter.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class MemberPresenter < Gitlab::View::Presenter::Delegated
-  presents :member
+  presents ::Member, as: :member
 
   def access_level_roles
     member.class.access_level_roles
diff --git a/app/presenters/members_presenter.rb b/app/presenters/members_presenter.rb
index 03ebea36d49b..b572cf962358 100644
--- a/app/presenters/members_presenter.rb
+++ b/app/presenters/members_presenter.rb
@@ -3,7 +3,7 @@
 class MembersPresenter < Gitlab::View::Presenter::Delegated
   include Enumerable
 
-  presents :members
+  presents nil, as: :members
 
   def to_ary
     to_a
diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb
index fc8a290f5f78..d19d4964524e 100644
--- a/app/presenters/merge_request_presenter.rb
+++ b/app/presenters/merge_request_presenter.rb
@@ -8,9 +8,11 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
   include ChecksCollaboration
   include Gitlab::Utils::StrongMemoize
 
+  delegator_override_with Gitlab::Utils::StrongMemoize # TODO: Remove `Gitlab::Utils::StrongMemoize` inclusion as it's duplicate
+
   APPROVALS_WIDGET_BASE_TYPE = 'base'
 
-  presents :merge_request
+  presents ::MergeRequest, as: :merge_request
 
   def ci_status
     if pipeline
@@ -183,6 +185,7 @@ def can_push_to_source_branch?
       .can_push_to_branch?(source_branch)
   end
 
+  delegator_override :can_remove_source_branch?
   def can_remove_source_branch?
     source_branch_exists? && merge_request.can_remove_source_branch?(current_user)
   end
@@ -202,6 +205,7 @@ def mergeable_discussions_state
     end
   end
 
+  delegator_override :subscribed?
   def subscribed?
     merge_request.subscribed?(current_user, merge_request.target_project)
   end
diff --git a/app/presenters/milestone_presenter.rb b/app/presenters/milestone_presenter.rb
index 6bf8203702fe..4084c8740f0e 100644
--- a/app/presenters/milestone_presenter.rb
+++ b/app/presenters/milestone_presenter.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class MilestonePresenter < Gitlab::View::Presenter::Delegated
-  presents :milestone
+  presents ::Milestone, as: :milestone
 
   def milestone_path
     url_builder.build(milestone, only_path: true)
diff --git a/app/presenters/pages_domain_presenter.rb b/app/presenters/pages_domain_presenter.rb
index 6ef89760bec1..0523f7024161 100644
--- a/app/presenters/pages_domain_presenter.rb
+++ b/app/presenters/pages_domain_presenter.rb
@@ -1,7 +1,9 @@
 # frozen_string_literal: true
 
 class PagesDomainPresenter < Gitlab::View::Presenter::Delegated
-  presents :pages_domain
+  presents ::PagesDomain, as: :pages_domain
+
+  delegator_override :subject # TODO: Fix `Gitlab::View::Presenter::Delegated#subject` not to override `PagesDomain#subject`.
 
   def needs_verification?
     Gitlab::CurrentSettings.pages_domain_verification_enabled? && unverified?
diff --git a/app/presenters/project_clusterable_presenter.rb b/app/presenters/project_clusterable_presenter.rb
index 920304e743ed..05344f2da965 100644
--- a/app/presenters/project_clusterable_presenter.rb
+++ b/app/presenters/project_clusterable_presenter.rb
@@ -4,6 +4,8 @@ class ProjectClusterablePresenter < ClusterablePresenter
   extend ::Gitlab::Utils::Override
   include ActionView::Helpers::UrlHelper
 
+  presents ::Project
+
   override :cluster_status_cluster_path
   def cluster_status_cluster_path(cluster, params = {})
     cluster_status_project_cluster_path(clusterable, cluster, params)
diff --git a/app/presenters/project_hook_presenter.rb b/app/presenters/project_hook_presenter.rb
index a65c7221b5a4..a696e9fd0ec8 100644
--- a/app/presenters/project_hook_presenter.rb
+++ b/app/presenters/project_hook_presenter.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class ProjectHookPresenter < Gitlab::View::Presenter::Delegated
-  presents :project_hook
+  presents ::ProjectHook, as: :project_hook
 
   def logs_details_path(log)
     project_hook_hook_log_path(project, self, log)
diff --git a/app/presenters/project_member_presenter.rb b/app/presenters/project_member_presenter.rb
index 17947266ed79..91d3ae96877c 100644
--- a/app/presenters/project_member_presenter.rb
+++ b/app/presenters/project_member_presenter.rb
@@ -1,6 +1,8 @@
 # frozen_string_literal: true
 
 class ProjectMemberPresenter < MemberPresenter
+  presents ::ProjectMember
+
   private
 
   def admin_member_permission
diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb
index 066f4786cff5..bbd8c715f5cb 100644
--- a/app/presenters/project_presenter.rb
+++ b/app/presenters/project_presenter.rb
@@ -12,7 +12,10 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
   include Gitlab::Utils::StrongMemoize
   include Gitlab::Experiment::Dsl
 
-  presents :project
+  delegator_override_with GitlabRoutingHelper # TODO: Remove `GitlabRoutingHelper` inclusion as it's duplicate
+  delegator_override_with Gitlab::Utils::StrongMemoize # TODO: Remove `Gitlab::Utils::StrongMemoize` inclusion as it's duplicate
+
+  presents ::Project, as: :project
 
   AnchorData = Struct.new(:is_link, :label, :link, :class_modifier, :icon, :itemprop, :data)
   MAX_TOPICS_TO_SHOW = 3
diff --git a/app/presenters/projects/import_export/project_export_presenter.rb b/app/presenters/projects/import_export/project_export_presenter.rb
index f56760b55df4..7b2ffb6d7557 100644
--- a/app/presenters/projects/import_export/project_export_presenter.rb
+++ b/app/presenters/projects/import_export/project_export_presenter.rb
@@ -5,16 +5,24 @@ module ImportExport
     class ProjectExportPresenter < Gitlab::View::Presenter::Delegated
       include ActiveModel::Serializers::JSON
 
-      presents :project
+      presents ::Project, as: :project
 
+      # TODO: Remove `ActiveModel::Serializers::JSON` inclusion as it's duplicate
+      delegator_override_with ActiveModel::Serializers::JSON
+      delegator_override_with ActiveModel::Naming
+      delegator_override :include_root_in_json, :include_root_in_json?
+
+      delegator_override :project_members
       def project_members
         super + converted_group_members
       end
 
+      delegator_override :description
       def description
         self.respond_to?(:override_description) ? override_description : super
       end
 
+      delegator_override :protected_branches
       def protected_branches
         project.exported_protected_branches
       end
diff --git a/app/presenters/projects/settings/deploy_keys_presenter.rb b/app/presenters/projects/settings/deploy_keys_presenter.rb
index 13290a8e632f..e3323b751881 100644
--- a/app/presenters/projects/settings/deploy_keys_presenter.rb
+++ b/app/presenters/projects/settings/deploy_keys_presenter.rb
@@ -5,7 +5,7 @@ module Settings
     class DeployKeysPresenter < Gitlab::View::Presenter::Simple
       include Gitlab::Utils::StrongMemoize
 
-      presents :project
+      presents ::Project, as: :project
       delegate :size, to: :enabled_keys, prefix: true
       delegate :size, to: :available_project_keys, prefix: true
       delegate :size, to: :available_public_keys, prefix: true
diff --git a/app/presenters/prometheus_alert_presenter.rb b/app/presenters/prometheus_alert_presenter.rb
index 99e24bdcdb9c..714329ede71e 100644
--- a/app/presenters/prometheus_alert_presenter.rb
+++ b/app/presenters/prometheus_alert_presenter.rb
@@ -3,7 +3,7 @@
 class PrometheusAlertPresenter < Gitlab::View::Presenter::Delegated
   include ActionView::Helpers::UrlHelper
 
-  presents :prometheus_alert
+  presents ::PrometheusAlert, as: :prometheus_alert
 
   def humanized_text
     operator_text =
diff --git a/app/presenters/release_presenter.rb b/app/presenters/release_presenter.rb
index ac27e997b41c..c919c7f4c60d 100644
--- a/app/presenters/release_presenter.rb
+++ b/app/presenters/release_presenter.rb
@@ -3,8 +3,10 @@
 class ReleasePresenter < Gitlab::View::Presenter::Delegated
   include ActionView::Helpers::UrlHelper
 
-  presents :release
+  presents ::Release, as: :release
 
+  # TODO: Remove `delegate` as it's redundant due to SimpleDelegator.
+  delegator_override :tag, :project
   delegate :project, :tag, to: :release
 
   def commit_path
@@ -51,6 +53,7 @@ def edit_url
     edit_project_release_url(project, release)
   end
 
+  delegator_override :assets_count
   def assets_count
     if can_download_code?
       release.assets_count
@@ -59,6 +62,7 @@ def assets_count
     end
   end
 
+  delegator_override :name
   def name
     can_download_code? ? release.name : "Release-#{release.id}"
   end
diff --git a/app/presenters/releases/evidence_presenter.rb b/app/presenters/releases/evidence_presenter.rb
index a00cbacb7d83..bdc053a303be 100644
--- a/app/presenters/releases/evidence_presenter.rb
+++ b/app/presenters/releases/evidence_presenter.rb
@@ -4,7 +4,7 @@ module Releases
   class EvidencePresenter < Gitlab::View::Presenter::Delegated
     include ActionView::Helpers::UrlHelper
 
-    presents :evidence
+    presents ::Releases::Evidence, as: :evidence
 
     def filepath
       release = evidence.release
diff --git a/app/presenters/search_service_presenter.rb b/app/presenters/search_service_presenter.rb
index ab43800b9f23..72f967b8beb1 100644
--- a/app/presenters/search_service_presenter.rb
+++ b/app/presenters/search_service_presenter.rb
@@ -3,7 +3,7 @@
 class SearchServicePresenter < Gitlab::View::Presenter::Delegated
   include RendersCommits
 
-  presents :search_service
+  presents ::SearchService, as: :search_service
 
   SCOPE_PRELOAD_METHOD = {
     projects: :with_web_entity_associations,
@@ -18,6 +18,7 @@ class SearchServicePresenter < Gitlab::View::Presenter::Delegated
 
   SORT_ENABLED_SCOPES = %w(issues merge_requests epics).freeze
 
+  delegator_override :search_objects
   def search_objects
     @search_objects ||= begin
       objects = search_service.search_objects(SCOPE_PRELOAD_METHOD[scope.to_sym])
diff --git a/app/presenters/sentry_error_presenter.rb b/app/presenters/sentry_error_presenter.rb
index 669bcb68b7cf..5862e54dfc7c 100644
--- a/app/presenters/sentry_error_presenter.rb
+++ b/app/presenters/sentry_error_presenter.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class SentryErrorPresenter < Gitlab::View::Presenter::Delegated
-  presents :error
+  presents nil, as: :error
 
   FrequencyStruct = Struct.new(:time, :count, keyword_init: true)
 
diff --git a/app/presenters/service_hook_presenter.rb b/app/presenters/service_hook_presenter.rb
index 8f2ba1a905f1..91911eb3dff2 100644
--- a/app/presenters/service_hook_presenter.rb
+++ b/app/presenters/service_hook_presenter.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class ServiceHookPresenter < Gitlab::View::Presenter::Delegated
-  presents :service_hook
+  presents ::ServiceHook, as: :service_hook
 
   def logs_details_path(log)
     project_service_hook_log_path(integration.project, integration, log)
diff --git a/app/presenters/snippet_blob_presenter.rb b/app/presenters/snippet_blob_presenter.rb
index ab8fc0f905b7..4072696eb890 100644
--- a/app/presenters/snippet_blob_presenter.rb
+++ b/app/presenters/snippet_blob_presenter.rb
@@ -3,6 +3,8 @@
 class SnippetBlobPresenter < BlobPresenter
   include GitlabRoutingHelper
 
+  presents ::SnippetBlob
+
   def rich_data
     return unless blob.rich_viewer
 
diff --git a/app/presenters/snippet_presenter.rb b/app/presenters/snippet_presenter.rb
index 695aa266e2ca..8badbe7f54ac 100644
--- a/app/presenters/snippet_presenter.rb
+++ b/app/presenters/snippet_presenter.rb
@@ -1,16 +1,18 @@
 # frozen_string_literal: true
 
 class SnippetPresenter < Gitlab::View::Presenter::Delegated
-  presents :snippet
+  presents ::Snippet, as: :snippet
 
   def raw_url
     url_builder.build(snippet, raw: true)
   end
 
+  delegator_override :ssh_url_to_repo
   def ssh_url_to_repo
     snippet.ssh_url_to_repo if snippet.repository_exists?
   end
 
+  delegator_override :http_url_to_repo
   def http_url_to_repo
     snippet.http_url_to_repo if snippet.repository_exists?
   end
@@ -31,6 +33,7 @@ def can_report_as_spam?
     snippet.submittable_as_spam_by?(current_user)
   end
 
+  delegator_override :blob
   def blob
     return snippet.blob if snippet.empty_repo?
 
diff --git a/app/presenters/terraform/modules_presenter.rb b/app/presenters/terraform/modules_presenter.rb
index 608f69e20197..9e9c6a5cd2b6 100644
--- a/app/presenters/terraform/modules_presenter.rb
+++ b/app/presenters/terraform/modules_presenter.rb
@@ -4,7 +4,7 @@ module Terraform
   class ModulesPresenter < Gitlab::View::Presenter::Simple
     attr_accessor :packages, :system
 
-    presents :modules
+    presents nil, as: :modules
 
     def initialize(packages, system)
       @packages = packages
diff --git a/app/presenters/todo_presenter.rb b/app/presenters/todo_presenter.rb
index 291be7848e25..cb8d37be22d8 100644
--- a/app/presenters/todo_presenter.rb
+++ b/app/presenters/todo_presenter.rb
@@ -1,5 +1,5 @@
 # frozen_string_literal: true
 
 class TodoPresenter < Gitlab::View::Presenter::Delegated
-  presents :todo
+  presents ::Todo, as: :todo
 end
diff --git a/app/presenters/tree_entry_presenter.rb b/app/presenters/tree_entry_presenter.rb
index 216b3b0d4c91..0b313d813601 100644
--- a/app/presenters/tree_entry_presenter.rb
+++ b/app/presenters/tree_entry_presenter.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class TreeEntryPresenter < Gitlab::View::Presenter::Delegated
-  presents :tree
+  presents nil, as: :tree
 
   def web_url
     Gitlab::Routing.url_helpers.project_tree_url(tree.repository.project, File.join(tree.commit_id, tree.path))
diff --git a/app/presenters/user_presenter.rb b/app/presenters/user_presenter.rb
index 7cd94082bacb..5a99f10b6e74 100644
--- a/app/presenters/user_presenter.rb
+++ b/app/presenters/user_presenter.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class UserPresenter < Gitlab::View::Presenter::Delegated
-  presents :user
+  presents ::User, as: :user
 
   def group_memberships
     should_be_private? ? GroupMember.none : user.group_members
diff --git a/app/presenters/web_hook_log_presenter.rb b/app/presenters/web_hook_log_presenter.rb
index fca03ddb5d76..a51665890737 100644
--- a/app/presenters/web_hook_log_presenter.rb
+++ b/app/presenters/web_hook_log_presenter.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class WebHookLogPresenter < Gitlab::View::Presenter::Delegated
-  presents :web_hook_log
+  presents ::WebHookLog, as: :web_hook_log
 
   def details_path
     web_hook.present.logs_details_path(self)
diff --git a/doc/development/reusing_abstractions.md b/doc/development/reusing_abstractions.md
index ded6b0743248..568e8a9d1235 100644
--- a/doc/development/reusing_abstractions.md
+++ b/doc/development/reusing_abstractions.md
@@ -183,6 +183,8 @@ queries they produce.
 Everything in `app/presenters`, used for exposing complex data to a Rails view,
 without having to create many instance variables.
 
+See [the documentation](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/presenters/README.md) for more information.
+
 ### Serializers
 
 Everything in `app/serializers`, used for presenting the response to a request,
diff --git a/ee/app/presenters/audit_event_presenter.rb b/ee/app/presenters/audit_event_presenter.rb
index 7c42cbb3a38c..48170b89c1e7 100644
--- a/ee/app/presenters/audit_event_presenter.rb
+++ b/ee/app/presenters/audit_event_presenter.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class AuditEventPresenter < Gitlab::View::Presenter::Simple
-  presents :audit_event
+  presents ::AuditEvent, as: :audit_event
 
   def author_name
     audit_event.author_name
diff --git a/ee/app/presenters/boards/epic_board_presenter.rb b/ee/app/presenters/boards/epic_board_presenter.rb
index 4b791a1e8930..a788a6709934 100644
--- a/ee/app/presenters/boards/epic_board_presenter.rb
+++ b/ee/app/presenters/boards/epic_board_presenter.rb
@@ -2,6 +2,6 @@
 
 module Boards
   class EpicBoardPresenter < Gitlab::View::Presenter::Delegated
-    presents :epic_board
+    presents ::Boards::EpicBoard, as: :epic_board
   end
 end
diff --git a/ee/app/presenters/dast/site_profile_presenter.rb b/ee/app/presenters/dast/site_profile_presenter.rb
index 7fe17745aaeb..ca1f40873a9c 100644
--- a/ee/app/presenters/dast/site_profile_presenter.rb
+++ b/ee/app/presenters/dast/site_profile_presenter.rb
@@ -5,7 +5,7 @@ class SiteProfilePresenter < Gitlab::View::Presenter::Delegated
     REDACTED_PASSWORD = '••••••••'
     REDACTED_REQUEST_HEADERS = '••••••••'
 
-    presents :site_profile
+    presents ::DastSiteProfile, as: :site_profile
 
     def password
       return unless site_profile.secret_variables.any? { |variable| variable.key == ::Dast::SiteProfileSecretVariable::PASSWORD }
diff --git a/ee/app/presenters/ee/ci/build_presenter.rb b/ee/app/presenters/ee/ci/build_presenter.rb
index 679224e7a397..1c739f66e599 100644
--- a/ee/app/presenters/ee/ci/build_presenter.rb
+++ b/ee/app/presenters/ee/ci/build_presenter.rb
@@ -4,7 +4,9 @@ module EE
   module Ci
     module BuildPresenter
       extend ActiveSupport::Concern
+      extend ::Gitlab::Utils::DelegatorOverride
 
+      delegator_override :retryable?
       def retryable?
         !merge_train_pipeline? && super
       end
diff --git a/ee/app/presenters/ee/ci/pipeline_presenter.rb b/ee/app/presenters/ee/ci/pipeline_presenter.rb
index d31cb811239c..e1b23cdc446e 100644
--- a/ee/app/presenters/ee/ci/pipeline_presenter.rb
+++ b/ee/app/presenters/ee/ci/pipeline_presenter.rb
@@ -4,6 +4,7 @@ module EE
   module Ci
     module PipelinePresenter
       extend ActiveSupport::Concern
+      extend ::Gitlab::Utils::DelegatorOverride
 
       def expose_security_dashboard?
         return false unless can?(current_user, :read_security_resource, pipeline.project)
@@ -18,6 +19,7 @@ def degradation_threshold(file_type)
         end
       end
 
+      delegator_override :retryable?
       def retryable?
         !merge_train_pipeline? && super
       end
diff --git a/ee/app/presenters/ee/label_presenter.rb b/ee/app/presenters/ee/label_presenter.rb
index fd3a4572077f..30180ca8e69e 100644
--- a/ee/app/presenters/ee/label_presenter.rb
+++ b/ee/app/presenters/ee/label_presenter.rb
@@ -2,6 +2,9 @@
 
 module EE
   module LabelPresenter
+    extend ::Gitlab::Utils::DelegatorOverride
+
+    delegator_override :scoped_label?
     def scoped_label?
       label.scoped_label? && context_subject && context_subject.feature_available?(:scoped_labels)
     end
diff --git a/ee/app/presenters/ee/merge_request_presenter.rb b/ee/app/presenters/ee/merge_request_presenter.rb
index 3a9dad8fb5d2..2a15d8241943 100644
--- a/ee/app/presenters/ee/merge_request_presenter.rb
+++ b/ee/app/presenters/ee/merge_request_presenter.rb
@@ -4,6 +4,9 @@ module EE
   module MergeRequestPresenter
     include ::VisibleApprovable
     extend ::Gitlab::Utils::Override
+    extend ::Gitlab::Utils::DelegatorOverride
+
+    delegator_override_with ::VisibleApprovable # TODO: Remove `::VisibleApprovable` inclusion as it's duplicate
 
     APPROVALS_WIDGET_FULL_TYPE = 'full'
 
@@ -33,6 +36,7 @@ def merge_immediately_docs_path
       help_page_path('ci/pipelines/merge_trains.md', anchor: 'immediately-merge-a-merge-request-with-a-merge-train')
     end
 
+    delegator_override :target_project
     def target_project
       merge_request.target_project.present(current_user: current_user)
     end
@@ -41,6 +45,7 @@ def code_owner_rules_with_users
       @code_owner_rules ||= merge_request.approval_rules.code_owner.with_users.to_a
     end
 
+    delegator_override :approver_groups
     def approver_groups
       ::ApproverGroup.filtered_approver_groups(merge_request.approver_groups, current_user)
     end
@@ -54,6 +59,7 @@ def approvals_widget_type
       expose_mr_approval_path? ? APPROVALS_WIDGET_FULL_TYPE : super
     end
 
+    delegator_override :missing_security_scan_types
     def missing_security_scan_types
       merge_request.missing_security_scan_types if expose_missing_security_scan_types?
     end
diff --git a/ee/app/presenters/ee/project_presenter.rb b/ee/app/presenters/ee/project_presenter.rb
index d82df27e156f..2cea06454d75 100644
--- a/ee/app/presenters/ee/project_presenter.rb
+++ b/ee/app/presenters/ee/project_presenter.rb
@@ -3,6 +3,7 @@
 module EE
   module ProjectPresenter
     extend ::Gitlab::Utils::Override
+    extend ::Gitlab::Utils::DelegatorOverride
 
     override :statistics_buttons
     def statistics_buttons(show_auto_devops_callout:)
@@ -13,6 +14,7 @@ def extra_statistics_buttons
       [sast_anchor_data.presence].compact
     end
 
+    delegator_override :approver_groups
     def approver_groups
       ::ApproverGroup.filtered_approver_groups(project.approver_groups, current_user)
     end
diff --git a/ee/app/presenters/epic_issue_presenter.rb b/ee/app/presenters/epic_issue_presenter.rb
index f54904196a56..a0338354556a 100644
--- a/ee/app/presenters/epic_issue_presenter.rb
+++ b/ee/app/presenters/epic_issue_presenter.rb
@@ -1,7 +1,8 @@
 # frozen_string_literal: true
 
 class EpicIssuePresenter < Gitlab::View::Presenter::Delegated
-  presents :issue
+  delegator_override :issue
+  presents ::EpicIssue, as: :issue
 
   def group_epic_issue_path(current_user)
     return unless can_admin_issue_link?(current_user)
diff --git a/ee/app/presenters/epic_presenter.rb b/ee/app/presenters/epic_presenter.rb
index 16e53e49134b..ae6bed4ebbb7 100644
--- a/ee/app/presenters/epic_presenter.rb
+++ b/ee/app/presenters/epic_presenter.rb
@@ -4,7 +4,7 @@ class EpicPresenter < Gitlab::View::Presenter::Delegated
   include GitlabRoutingHelper
   include EntityDateHelper
 
-  presents :epic
+  presents ::Epic, as: :epic
 
   def show_data(base_data: {}, author_icon: nil)
     {
@@ -35,6 +35,7 @@ def epic_reference(full: false)
     end
   end
 
+  delegator_override :subscribed?
   def subscribed?
     epic.subscribed?(current_user)
   end
diff --git a/ee/app/presenters/iteration_presenter.rb b/ee/app/presenters/iteration_presenter.rb
index 4c2763b3345f..63871c7cc432 100644
--- a/ee/app/presenters/iteration_presenter.rb
+++ b/ee/app/presenters/iteration_presenter.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class IterationPresenter < Gitlab::View::Presenter::Delegated
-  presents :iteration
+  presents ::Iteration, as: :iteration
 
   def iteration_path
     url_builder.build(iteration, only_path: true)
diff --git a/ee/app/presenters/merge_request_approver_presenter.rb b/ee/app/presenters/merge_request_approver_presenter.rb
index 6c0e5423d1be..1efa1a71864a 100644
--- a/ee/app/presenters/merge_request_approver_presenter.rb
+++ b/ee/app/presenters/merge_request_approver_presenter.rb
@@ -12,7 +12,7 @@ class MergeRequestApproverPresenter < Gitlab::View::Presenter::Simple
   include ActionView::RecordIdentifier
   include Gitlab::Utils::StrongMemoize
 
-  presents :merge_request
+  presents ::MergeRequest, as: :merge_request
 
   attr_reader :skip_user
 
diff --git a/ee/app/presenters/projects/security/configuration_presenter.rb b/ee/app/presenters/projects/security/configuration_presenter.rb
index d32e3a706f6d..2ecb20bcf05b 100644
--- a/ee/app/presenters/projects/security/configuration_presenter.rb
+++ b/ee/app/presenters/projects/security/configuration_presenter.rb
@@ -7,7 +7,9 @@ class ConfigurationPresenter < Gitlab::View::Presenter::Delegated
       include AutoDevopsHelper
       include LatestPipelineInformation
 
-      presents :project
+      delegator_override_with Gitlab::Utils::StrongMemoize # TODO: Remove `Gitlab::Utils::StrongMemoize` inclusion as it's duplicate
+
+      presents ::Project, as: :project
 
       def to_h
         {
diff --git a/ee/app/presenters/security/scan_presenter.rb b/ee/app/presenters/security/scan_presenter.rb
index 9f59738d1446..36871023b980 100644
--- a/ee/app/presenters/security/scan_presenter.rb
+++ b/ee/app/presenters/security/scan_presenter.rb
@@ -4,8 +4,9 @@ module Security
   class ScanPresenter < Gitlab::View::Presenter::Delegated
     ERROR_MESSAGE_FORMAT = '[%<type>s] %<message>s'
 
-    presents :scan
+    presents ::Security::Scan, as: :scan
 
+    delegator_override :errors
     def errors
       info['errors'].to_a.map { |error| format(ERROR_MESSAGE_FORMAT, error.symbolize_keys) }
     end
diff --git a/ee/app/presenters/subscription_presenter.rb b/ee/app/presenters/subscription_presenter.rb
index b726876e4aa7..aa42356c72db 100644
--- a/ee/app/presenters/subscription_presenter.rb
+++ b/ee/app/presenters/subscription_presenter.rb
@@ -3,7 +3,7 @@
 class SubscriptionPresenter < Gitlab::View::Presenter::Delegated
   GRACE_PERIOD_EXTENSION_DAYS = 14.days
 
-  presents :subscription
+  presents ::Subscription, as: :subscription
 
   def block_changes?
     will_block_changes? && (block_changes_at < Date.today)
diff --git a/ee/app/presenters/subscriptions/new_plan_presenter.rb b/ee/app/presenters/subscriptions/new_plan_presenter.rb
index 90502c66c63e..858c22dec848 100644
--- a/ee/app/presenters/subscriptions/new_plan_presenter.rb
+++ b/ee/app/presenters/subscriptions/new_plan_presenter.rb
@@ -2,11 +2,14 @@
 
 module Subscriptions
   class NewPlanPresenter < Gitlab::View::Presenter::Delegated
+    presents ::Plan
+
     NEW_PLAN_TITLES = {
       silver: 'Premium (Formerly Silver)',
       gold: 'Ultimate (Formerly Gold)'
     }.freeze
 
+    delegator_override :title
     def title
       NEW_PLAN_TITLES.fetch(plan_key, super)
     end
diff --git a/ee/app/presenters/vulnerabilities/finding_presenter.rb b/ee/app/presenters/vulnerabilities/finding_presenter.rb
index 9235f696b167..c3e094413aa4 100644
--- a/ee/app/presenters/vulnerabilities/finding_presenter.rb
+++ b/ee/app/presenters/vulnerabilities/finding_presenter.rb
@@ -2,7 +2,7 @@
 
 module Vulnerabilities
   class FindingPresenter < Gitlab::View::Presenter::Delegated
-    presents :finding
+    presents ::Vulnerabilities::Finding, as: :finding
 
     def title
       name
diff --git a/ee/app/presenters/vulnerability_presenter.rb b/ee/app/presenters/vulnerability_presenter.rb
index 295462cc4faa..eadf6a7228f3 100644
--- a/ee/app/presenters/vulnerability_presenter.rb
+++ b/ee/app/presenters/vulnerability_presenter.rb
@@ -1,12 +1,14 @@
 # frozen_string_literal: true
 
 class VulnerabilityPresenter < Gitlab::View::Presenter::Delegated
-  presents :vulnerability
+  presents ::Vulnerability, as: :vulnerability
 
+  delegator_override :links
   def links
     vulnerability.links.map(&:with_indifferent_access)
   end
 
+  delegator_override :remediations
   def remediations
     vulnerability.remediations.to_a.compact.map(&:with_indifferent_access)
   end
@@ -31,6 +33,7 @@ def raw_path
     path_with_line_number(project_raw_path(vulnerability.project, File.join(pipeline_branch, file)))
   end
 
+  delegator_override :blob_path
   def blob_path
     return unless file
 
@@ -52,6 +55,7 @@ def jira_issue_description
     )
   end
 
+  delegator_override :description
   def description
     vulnerability.description || finding.description
   end
diff --git a/lib/gitlab/utils/delegator_override.rb b/lib/gitlab/utils/delegator_override.rb
new file mode 100644
index 000000000000..15ba29d39160
--- /dev/null
+++ b/lib/gitlab/utils/delegator_override.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module Gitlab
+  module Utils
+    # This module is to validate that delegator classes (`SimpleDelegator`) do not
+    # accidentally override important logic on the fabricated object.
+    module DelegatorOverride
+      def delegator_target(target_class)
+        return unless ENV['STATIC_VERIFICATION']
+
+        unless self < ::SimpleDelegator
+          raise ArgumentError, "'#{self}' is not a subclass of 'SimpleDelegator' class."
+        end
+
+        DelegatorOverride.validator(self).add_target(target_class)
+      end
+
+      def delegator_override(*names)
+        return unless ENV['STATIC_VERIFICATION']
+        raise TypeError unless names.all? { |n| n.is_a?(Symbol) }
+
+        DelegatorOverride.validator(self).add_allowlist(names)
+      end
+
+      def delegator_override_with(mod)
+        return unless ENV['STATIC_VERIFICATION']
+        raise TypeError unless mod.is_a?(Module)
+
+        DelegatorOverride.validator(self).add_allowlist(mod.instance_methods)
+      end
+
+      def self.validator(delegator_class)
+        validators[delegator_class] ||= Validator.new(delegator_class)
+      end
+
+      def self.validators
+        @validators ||= {}
+      end
+
+      def self.verify!
+        validators.each_value do |validator|
+          validator.expand_on_ancestors(validators)
+          validator.validate_overrides!
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/utils/delegator_override/error.rb b/lib/gitlab/utils/delegator_override/error.rb
new file mode 100644
index 000000000000..dfe8d5468b47
--- /dev/null
+++ b/lib/gitlab/utils/delegator_override/error.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Gitlab
+  module Utils
+    module DelegatorOverride
+      class Error
+        attr_accessor :method_name, :target_class, :target_location, :delegator_class, :delegator_location
+
+        def initialize(method_name, target_class, target_location, delegator_class, delegator_location)
+          @method_name = method_name
+          @target_class = target_class
+          @target_location = target_location
+          @delegator_class = delegator_class
+          @delegator_location = delegator_location
+        end
+
+        def to_s
+          "#{delegator_class}##{method_name} is overriding #{target_class}##{method_name}. delegator_location: #{delegator_location} target_location: #{target_location}"
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/utils/delegator_override/validator.rb b/lib/gitlab/utils/delegator_override/validator.rb
new file mode 100644
index 000000000000..825b3efa2034
--- /dev/null
+++ b/lib/gitlab/utils/delegator_override/validator.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+module Gitlab
+  module Utils
+    module DelegatorOverride
+      class Validator
+        UnexpectedDelegatorOverrideError = Class.new(StandardError)
+
+        attr_reader :delegator_class, :target_classes
+
+        OVERRIDE_ERROR_MESSAGE = <<~EOS
+          We've detected that the delegator is overriding a specific method(s) on the target class.
+          Please make sure if it's intentional and handle this error accordingly.
+          See https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/presenters/README.md#validate-accidental-overrides for more information.
+        EOS
+
+        def initialize(delegator_class)
+          @delegator_class = delegator_class
+          @target_classes = []
+        end
+
+        def add_allowlist(names)
+          allowed_method_names.concat(names)
+        end
+
+        def allowed_method_names
+          @allowed_method_names ||= []
+        end
+
+        def add_target(target_class)
+          @target_classes << target_class if target_class
+        end
+
+        # This will make sure allowlist we put into ancestors are all included
+        def expand_on_ancestors(validators)
+          delegator_class.ancestors.each do |ancestor|
+            next if delegator_class == ancestor # ancestor includes itself
+
+            validator_ancestor = validators[ancestor]
+
+            next unless validator_ancestor
+
+            add_allowlist(validator_ancestor.allowed_method_names)
+          end
+        end
+
+        def validate_overrides!
+          return if target_classes.empty?
+
+          errors = []
+
+          # Workaround to fully load the instance methods in the target class.
+          # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69823#note_678887402
+          target_classes.map(&:new) rescue nil
+
+          (delegator_class.instance_methods - allowlist).each do |method_name|
+            target_classes.each do |target_class|
+              next unless target_class.instance_methods.include?(method_name)
+
+              errors << generate_error(method_name, target_class, delegator_class)
+            end
+          end
+
+          return if errors.empty?
+
+          details = errors.map { |error| "- #{error}" }.join("\n")
+
+          raise UnexpectedDelegatorOverrideError,
+            <<~TEXT
+              #{OVERRIDE_ERROR_MESSAGE}
+              Here are the conflict details.
+
+              #{details}
+            TEXT
+        end
+
+        private
+
+        def generate_error(method_name, target_class, delegator_class)
+          target_location = extract_location(target_class, method_name)
+          delegator_location = extract_location(delegator_class, method_name)
+          Error.new(method_name, target_class, target_location, delegator_class, delegator_location)
+        end
+
+        def extract_location(klass, method_name)
+          klass.instance_method(method_name).source_location&.join(':') || 'unknown'
+        end
+
+        def allowlist
+          [].tap do |allowed|
+            allowed.concat(allowed_method_names)
+            allowed.concat(Object.instance_methods)
+            allowed.concat(::Delegator.instance_methods)
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/view/presenter/base.rb b/lib/gitlab/view/presenter/base.rb
index 9dc687f7740f..3bacad720500 100644
--- a/lib/gitlab/view/presenter/base.rb
+++ b/lib/gitlab/view/presenter/base.rb
@@ -47,8 +47,18 @@ def presenter?
             true
           end
 
-          def presents(name)
-            define_method(name) { subject }
+          def presents(*target_classes, as: nil)
+            if target_classes.any? { |k| k.is_a?(Symbol) }
+              raise ArgumentError, "Unsupported target class type: #{target_classes}."
+            end
+
+            if self < ::Gitlab::View::Presenter::Delegated
+              target_classes.each { |k| delegator_target(k) }
+            elsif self < ::Gitlab::View::Presenter::Simple
+              # no-op
+            end
+
+            define_method(as) { subject } if as
           end
         end
       end
diff --git a/lib/gitlab/view/presenter/delegated.rb b/lib/gitlab/view/presenter/delegated.rb
index d14f8cc4e5ed..259cf0cf457f 100644
--- a/lib/gitlab/view/presenter/delegated.rb
+++ b/lib/gitlab/view/presenter/delegated.rb
@@ -4,7 +4,18 @@ module Gitlab
   module View
     module Presenter
       class Delegated < SimpleDelegator
+        extend ::Gitlab::Utils::DelegatorOverride
+
+        # TODO: Stop including auxiliary methods/modules in `Presenter::Base` as
+        # it overrides many methods in the Active Record models.
+        # See https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/presenters/README.md#validate-accidental-overrides
+        # for more information.
         include Gitlab::View::Presenter::Base
+        delegator_override_with Gitlab::Routing.url_helpers
+        delegator_override :can?
+        delegator_override :declarative_policy_delegate
+        delegator_override :present
+        delegator_override :web_url
 
         def initialize(subject, **attributes)
           @subject = subject
diff --git a/lib/tasks/lint.rake b/lib/tasks/lint.rake
index 976ec0890115..7d1721f1df8d 100644
--- a/lib/tasks/lint.rake
+++ b/lib/tasks/lint.rake
@@ -12,6 +12,7 @@ unless Rails.env.production?
       dev:load
     ] do
       Gitlab::Utils::Override.verify!
+      Gitlab::Utils::DelegatorOverride.verify!
     end
 
     desc "GitLab | Lint | Lint JavaScript files using ESLint"
diff --git a/spec/lib/gitlab/utils/delegator_override/error_spec.rb b/spec/lib/gitlab/utils/delegator_override/error_spec.rb
new file mode 100644
index 000000000000..59b67676eff4
--- /dev/null
+++ b/spec/lib/gitlab/utils/delegator_override/error_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe Gitlab::Utils::DelegatorOverride::Error do
+  let(:error) { described_class.new('foo', 'Target', '/path/to/target', 'Delegator', '/path/to/delegator') }
+
+  describe '#to_s' do
+    subject { error.to_s }
+
+    it { is_expected.to eq("Delegator#foo is overriding Target#foo. delegator_location: /path/to/delegator target_location: /path/to/target") }
+  end
+end
diff --git a/spec/lib/gitlab/utils/delegator_override/validator_spec.rb b/spec/lib/gitlab/utils/delegator_override/validator_spec.rb
new file mode 100644
index 000000000000..4cd1b18de82b
--- /dev/null
+++ b/spec/lib/gitlab/utils/delegator_override/validator_spec.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe Gitlab::Utils::DelegatorOverride::Validator do
+  let(:delegator_class) do
+    Class.new(::SimpleDelegator) do
+      extend(::Gitlab::Utils::DelegatorOverride)
+
+      def foo
+      end
+    end.prepend(ee_delegator_extension)
+  end
+
+  let(:ee_delegator_extension) do
+    Module.new do
+      extend(::Gitlab::Utils::DelegatorOverride)
+
+      def bar
+      end
+    end
+  end
+
+  let(:target_class) do
+    Class.new do
+      def foo
+      end
+
+      def bar
+      end
+    end
+  end
+
+  let(:validator) { described_class.new(delegator_class) }
+
+  describe '#add_allowlist' do
+    it 'adds a method name to the allowlist' do
+      validator.add_allowlist([:foo])
+
+      expect(validator.allowed_method_names).to contain_exactly(:foo)
+    end
+  end
+
+  describe '#add_target' do
+    it 'adds the target class' do
+      validator.add_target(target_class)
+
+      expect(validator.target_classes).to contain_exactly(target_class)
+    end
+  end
+
+  describe '#expand_on_ancestors' do
+    it 'adds the allowlist in the ancestors' do
+      ancestor_validator = described_class.new(ee_delegator_extension)
+      ancestor_validator.add_allowlist([:bar])
+      validator.expand_on_ancestors( { ee_delegator_extension => ancestor_validator })
+
+      expect(validator.allowed_method_names).to contain_exactly(:bar)
+    end
+  end
+
+  describe '#validate_overrides!' do
+    before do
+      validator.add_target(target_class)
+    end
+
+    it 'does not raise an error when the overrides are allowed' do
+      validator.add_allowlist([:foo])
+      ancestor_validator = described_class.new(ee_delegator_extension)
+      ancestor_validator.add_allowlist([:bar])
+      validator.expand_on_ancestors( { ee_delegator_extension => ancestor_validator })
+
+      expect { validator.validate_overrides! }.not_to raise_error
+    end
+
+    it 'raises an error when there is an override' do
+      expect { validator.validate_overrides! }
+        .to raise_error(described_class::UnexpectedDelegatorOverrideError)
+    end
+  end
+end
diff --git a/spec/lib/gitlab/utils/delegator_override_spec.rb b/spec/lib/gitlab/utils/delegator_override_spec.rb
new file mode 100644
index 000000000000..af4c7fa5d8ec
--- /dev/null
+++ b/spec/lib/gitlab/utils/delegator_override_spec.rb
@@ -0,0 +1,97 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe Gitlab::Utils::DelegatorOverride do
+  let(:delegator_class) do
+    Class.new(::SimpleDelegator) do
+      extend(::Gitlab::Utils::DelegatorOverride)
+
+      def foo
+      end
+    end
+  end
+
+  let(:target_class) do
+    Class.new do
+      def foo
+      end
+
+      def bar
+      end
+    end
+  end
+
+  let(:dummy_module) do
+    Module.new do
+      def foobar
+      end
+    end
+  end
+
+  before do
+    stub_env('STATIC_VERIFICATION', 'true')
+  end
+
+  describe '.delegator_target' do
+    subject { delegator_class.delegator_target(target_class) }
+
+    it 'sets the delegator target to the validator' do
+      expect(described_class.validator(delegator_class))
+        .to receive(:add_target).with(target_class)
+
+      subject
+    end
+
+    context 'when the class does not inherit SimpleDelegator' do
+      let(:delegator_class) do
+        Class.new do
+          extend(::Gitlab::Utils::DelegatorOverride)
+        end
+      end
+
+      it 'raises an error' do
+        expect { subject }.to raise_error(ArgumentError, /not a subclass of 'SimpleDelegator' class/)
+      end
+    end
+  end
+
+  describe '.delegator_override' do
+    subject { delegator_class.delegator_override(:foo) }
+
+    it 'adds the method name to the allowlist' do
+      expect(described_class.validator(delegator_class))
+        .to receive(:add_allowlist).with([:foo])
+
+      subject
+    end
+  end
+
+  describe '.delegator_override_with' do
+    subject { delegator_class.delegator_override_with(dummy_module) }
+
+    it 'adds the method names of the module to the allowlist' do
+      expect(described_class.validator(delegator_class))
+        .to receive(:add_allowlist).with([:foobar])
+
+      subject
+    end
+  end
+
+  describe '.verify!' do
+    subject { described_class.verify! }
+
+    it 'does not raise an error when an override is in allowlist' do
+      delegator_class.delegator_target(target_class)
+      delegator_class.delegator_override(:foo)
+
+      expect { subject }.not_to raise_error
+    end
+
+    it 'raises an error when there is an override' do
+      delegator_class.delegator_target(target_class)
+
+      expect { subject }.to raise_error(Gitlab::Utils::DelegatorOverride::Validator::UnexpectedDelegatorOverrideError)
+    end
+  end
+end
diff --git a/spec/lib/gitlab/view/presenter/base_spec.rb b/spec/lib/gitlab/view/presenter/base_spec.rb
index 97d5e2b280de..a7083bd27223 100644
--- a/spec/lib/gitlab/view/presenter/base_spec.rb
+++ b/spec/lib/gitlab/view/presenter/base_spec.rb
@@ -18,11 +18,43 @@
 
   describe '.presents' do
     it 'exposes #subject with the given keyword' do
-      presenter_class.presents(:foo)
+      presenter_class.presents(Object, as: :foo)
       presenter = presenter_class.new(project)
 
       expect(presenter.foo).to eq(project)
     end
+
+    it 'raises an error when symbol is passed' do
+      expect { presenter_class.presents(:foo) }.to raise_error(ArgumentError)
+    end
+
+    context 'when the presenter class inherits Presenter::Delegated' do
+      let(:presenter_class) do
+        Class.new(::Gitlab::View::Presenter::Delegated) do
+          include(::Gitlab::View::Presenter::Base)
+        end
+      end
+
+      it 'sets the delegator target' do
+        expect(presenter_class).to receive(:delegator_target).with(Object)
+
+        presenter_class.presents(Object, as: :foo)
+      end
+    end
+
+    context 'when the presenter class inherits Presenter::Simple' do
+      let(:presenter_class) do
+        Class.new(::Gitlab::View::Presenter::Simple) do
+          include(::Gitlab::View::Presenter::Base)
+        end
+      end
+
+      it 'does not set the delegator target' do
+        expect(presenter_class).not_to receive(:delegator_target).with(Object)
+
+        presenter_class.presents(Object, as: :foo)
+      end
+    end
   end
 
   describe '#can?' do
-- 
GitLab