From ab2127cf39b78b9bf9ce8a30551469670e15a2df Mon Sep 17 00:00:00 2001
From: Doug Stull <dstull@gitlab.com>
Date: Thu, 7 Nov 2024 14:59:25 -0500
Subject: [PATCH] Add custom access denied page for ultimate trials in-app

- more helpful UX for the user.
- only deny if the namespace_id is provided and is not
  eligible. Otherwise do not observe access denied as
  this trials path can create trials also

Changelog: other
EE: true
---
 .rubocop.yml                                  |  1 +
 .rubocop_todo/rails/strong_params.yml         |  1 -
 .../gitlab_subscriptions/trials/index.js      |  3 +
 .../gitlab_subscriptions/trials/duo_common.rb |  2 +-
 .../gitlab_subscriptions/trials_controller.rb | 39 ++++++--
 ee/app/models/gitlab_subscriptions/trials.rb  | 11 +++
 .../gitlab_subscriptions/trials/add_ons.rb    | 13 ---
 .../trials/base_create_add_on_service.rb      |  2 +-
 .../trials/create_service.rb                  |  6 +-
 .../trials/access_denied_spec.rb              | 11 +++
 .../trials/add_ons_spec.rb                    | 30 ------
 .../gitlab_subscriptions/trials_spec.rb       | 41 ++++++++
 .../trials_controller_spec.rb                 | 93 ++++++++++---------
 13 files changed, 147 insertions(+), 106 deletions(-)
 create mode 100644 ee/app/assets/javascripts/pages/gitlab_subscriptions/trials/index.js
 delete mode 100644 ee/app/models/gitlab_subscriptions/trials/add_ons.rb
 create mode 100644 ee/spec/features/gitlab_subscriptions/trials/access_denied_spec.rb
 delete mode 100644 ee/spec/models/gitlab_subscriptions/trials/add_ons_spec.rb

diff --git a/.rubocop.yml b/.rubocop.yml
index 2adbb1a270ce..60d2d8fd37f0 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -556,6 +556,7 @@ Gitlab/RSpec/AvoidSetup:
   Include:
     - 'ee/spec/features/registrations/saas/**/*'
     - 'ee/spec/features/gitlab_subscriptions/trials/creation_*'
+    - 'ee/spec/features/gitlab_subscriptions/trials/access_denied_spec.rb'
     - 'ee/spec/features/gitlab_subscriptions/trials/duo_pro/**/*'
     - 'ee/spec/features/gitlab_subscriptions/trials/duo_enterprise/**/*'
 
diff --git a/.rubocop_todo/rails/strong_params.yml b/.rubocop_todo/rails/strong_params.yml
index dec7be0263a3..1ec833b7a503 100644
--- a/.rubocop_todo/rails/strong_params.yml
+++ b/.rubocop_todo/rails/strong_params.yml
@@ -264,7 +264,6 @@ Rails/StrongParams:
     - 'ee/app/controllers/ee/sessions_controller.rb'
     - 'ee/app/controllers/ee/users_controller.rb'
     - 'ee/app/controllers/gitlab_subscriptions/subscriptions_controller.rb'
-    - 'ee/app/controllers/gitlab_subscriptions/trials_controller.rb'
     - 'ee/app/controllers/groups/analytics/application_controller.rb'
     - 'ee/app/controllers/groups/analytics/ci_cd_analytics_controller.rb'
     - 'ee/app/controllers/groups/analytics/coverage_reports_controller.rb'
diff --git a/ee/app/assets/javascripts/pages/gitlab_subscriptions/trials/index.js b/ee/app/assets/javascripts/pages/gitlab_subscriptions/trials/index.js
new file mode 100644
index 000000000000..780f47d9f06c
--- /dev/null
+++ b/ee/app/assets/javascripts/pages/gitlab_subscriptions/trials/index.js
@@ -0,0 +1,3 @@
+import initializeGoBack from 'ee/trials/init_go_back';
+
+initializeGoBack();
diff --git a/ee/app/controllers/concerns/gitlab_subscriptions/trials/duo_common.rb b/ee/app/controllers/concerns/gitlab_subscriptions/trials/duo_common.rb
index 0772fdea890d..f4c429d64bf7 100644
--- a/ee/app/controllers/concerns/gitlab_subscriptions/trials/duo_common.rb
+++ b/ee/app/controllers/concerns/gitlab_subscriptions/trials/duo_common.rb
@@ -28,7 +28,7 @@ def check_trial_eligibility!
       def eligible_namespaces_exist?
         return false if eligible_namespaces.none?
 
-        GitlabSubscriptions::Trials::AddOns.eligible_namespace?(general_params[:namespace_id], eligible_namespaces)
+        GitlabSubscriptions::Trials.eligible_namespace?(general_params[:namespace_id], eligible_namespaces)
       end
 
       def namespace
diff --git a/ee/app/controllers/gitlab_subscriptions/trials_controller.rb b/ee/app/controllers/gitlab_subscriptions/trials_controller.rb
index 668c2bad5874..ecfb7f16d545 100644
--- a/ee/app/controllers/gitlab_subscriptions/trials_controller.rb
+++ b/ee/app/controllers/gitlab_subscriptions/trials_controller.rb
@@ -11,17 +11,16 @@ class TrialsController < ApplicationController
     layout 'minimal'
 
     skip_before_action :set_confirm_warning
-    before_action :check_feature_available!
     before_action :authenticate_user!
-    # https://gitlab.com/gitlab-org/gitlab/-/issues/500686 will likely move this to being performed
-    # during an eligibility check similar to Trials::DuoEnterpriseController.
-    before_action :eligible_namespaces
+    before_action :check_feature_available!
+    before_action :check_trial_eligibility!
+    before_action :eligible_namespaces # needed when namespace_id isn't provided or is 0(new group)
 
     feature_category :plan_provisioning
     urgency :low
 
     def new
-      if params[:step] == GitlabSubscriptions::Trials::CreateService::TRIAL
+      if general_params[:step] == GitlabSubscriptions::Trials::CreateService::TRIAL
         render :step_namespace
       else
         render :step_lead
@@ -30,7 +29,7 @@ def new
 
     def create
       @result = GitlabSubscriptions::Trials::CreateService.new(
-        step: params[:step], lead_params: lead_params, trial_params: trial_params, user: current_user
+        step: general_params[:step], lead_params: lead_params, trial_params: trial_params, user: current_user
       ).execute
 
       if @result.success?
@@ -57,12 +56,12 @@ def create
         render :step_lead_failed
       elsif @result.reason == GitlabSubscriptions::Trials::CreateService::NAMESPACE_CREATE_FAILED
         # namespace creation failed
-        params[:namespace_id] = @result.payload[:namespace_id]
+        params[:namespace_id] = @result.payload[:namespace_id] # rubocop:disable Rails/StrongParams -- Not working for assignment
 
         render :step_namespace_failed
       else
         # trial creation failed
-        params[:namespace_id] = @result.payload[:namespace_id]
+        params[:namespace_id] = @result.payload[:namespace_id] # rubocop:disable Rails/StrongParams -- Not working for assignment
 
         render :trial_failed
       end
@@ -82,11 +81,15 @@ def authenticate_user!
       return if current_user
 
       redirect_to(
-        new_trial_registration_path(::Onboarding::Status.glm_tracking_params(params)),
+        new_trial_registration_path(::Onboarding::Status.glm_tracking_params(params)), # rubocop:disable Rails/StrongParams -- method performs strong params
         alert: I18n.t('devise.failure.unauthenticated')
       )
     end
 
+    def general_params
+      params.permit(:step)
+    end
+
     def lead_params
       params.permit(
         *::Onboarding::Status::GLM_PARAMS,
@@ -105,13 +108,29 @@ def eligible_namespaces
     end
 
     def discover_group_security_flow?
-      %w[discover-group-security discover-project-security].include?(params[:glm_content])
+      %w[discover-group-security discover-project-security].include?(trial_params[:glm_content])
     end
 
     def check_feature_available!
       render_404 unless ::Gitlab::Saas.feature_available?(:subscriptions_trials)
     end
 
+    def check_trial_eligibility!
+      return unless should_check_eligibility?
+      return if eligible_for_trial?
+
+      render 'gitlab_subscriptions/trials/duo/access_denied', status: :forbidden
+    end
+
+    def should_check_eligibility?
+      namespace_id = trial_params[:namespace_id]
+      namespace_id.present? && !GitlabSubscriptions::Trials.creating_group_trigger?(namespace_id)
+    end
+
+    def eligible_for_trial?
+      GitlabSubscriptions::Trials.eligible_namespace?(trial_params[:namespace_id], eligible_namespaces)
+    end
+
     def success_flash_message(add_on_purchase)
       if discover_group_security_flow?
         s_("BillingPlans|Congratulations, your free trial is activated.")
diff --git a/ee/app/models/gitlab_subscriptions/trials.rb b/ee/app/models/gitlab_subscriptions/trials.rb
index f58c84ecc71f..704ab487119d 100644
--- a/ee/app/models/gitlab_subscriptions/trials.rb
+++ b/ee/app/models/gitlab_subscriptions/trials.rb
@@ -8,6 +8,17 @@ def self.single_eligible_namespace?(eligible_namespaces)
       eligible_namespaces.count == 1
     end
 
+    def self.creating_group_trigger?(namespace_id)
+      # The value of 0 is the option in the select for creating a new group
+      namespace_id.to_s == '0'
+    end
+
+    def self.eligible_namespace?(namespace_id, eligible_namespaces)
+      return true if namespace_id.blank?
+
+      namespace_id.to_i.in?(eligible_namespaces.pluck_primary_key)
+    end
+
     def self.namespace_eligible?(namespace)
       namespace_plan_eligible?(namespace) && namespace_add_on_eligible?(namespace)
     end
diff --git a/ee/app/models/gitlab_subscriptions/trials/add_ons.rb b/ee/app/models/gitlab_subscriptions/trials/add_ons.rb
deleted file mode 100644
index 31ffa079e97a..000000000000
--- a/ee/app/models/gitlab_subscriptions/trials/add_ons.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-module GitlabSubscriptions
-  module Trials
-    module AddOns
-      def self.eligible_namespace?(namespace_id, eligible_namespaces)
-        return true if namespace_id.blank?
-
-        namespace_id.to_i.in?(eligible_namespaces.pluck_primary_key)
-      end
-    end
-  end
-end
diff --git a/ee/app/services/gitlab_subscriptions/trials/base_create_add_on_service.rb b/ee/app/services/gitlab_subscriptions/trials/base_create_add_on_service.rb
index 4019dfebc8b4..db3eab3a6cea 100644
--- a/ee/app/services/gitlab_subscriptions/trials/base_create_add_on_service.rb
+++ b/ee/app/services/gitlab_subscriptions/trials/base_create_add_on_service.rb
@@ -7,7 +7,7 @@ class BaseCreateAddOnService < ::GitlabSubscriptions::Trials::BaseCreateService
 
       override :execute
       def execute
-        return not_found unless AddOns.eligible_namespace?(trial_params[:namespace_id], namespaces_eligible_for_trial)
+        return not_found unless Trials.eligible_namespace?(trial_params[:namespace_id], namespaces_eligible_for_trial)
 
         super
       end
diff --git a/ee/app/services/gitlab_subscriptions/trials/create_service.rb b/ee/app/services/gitlab_subscriptions/trials/create_service.rb
index 7ffaabd83978..bb7ff914085e 100644
--- a/ee/app/services/gitlab_subscriptions/trials/create_service.rb
+++ b/ee/app/services/gitlab_subscriptions/trials/create_service.rb
@@ -8,10 +8,8 @@ class CreateService < BaseCreateService
       private
 
       def trial_flow
-        # The value of 0 is the option in the select for creating a new group
-        create_new_group_selected = trial_params[:namespace_id] == '0'
-
-        if trial_params[:namespace_id].present? && !create_new_group_selected
+        if trial_params[:namespace_id].present? &&
+            !GitlabSubscriptions::Trials.creating_group_trigger?(trial_params[:namespace_id])
           existing_namespace_flow
         elsif trial_params.key?(:new_group_name)
           create_group_flow
diff --git a/ee/spec/features/gitlab_subscriptions/trials/access_denied_spec.rb b/ee/spec/features/gitlab_subscriptions/trials/access_denied_spec.rb
new file mode 100644
index 000000000000..3dfeb489e82a
--- /dev/null
+++ b/ee/spec/features/gitlab_subscriptions/trials/access_denied_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Ultimate trial access denied flow', :saas_trial, :js, feature_category: :acquisition do
+  # rubocop:disable Gitlab/RSpec/AvoidSetup -- reuse of common flow
+  it_behaves_like 'duo access denied flow' do
+    let(:duo_path) { new_trial_path(namespace_id: non_existing_record_id) }
+  end
+  # rubocop:enable Gitlab/RSpec/AvoidSetup
+end
diff --git a/ee/spec/models/gitlab_subscriptions/trials/add_ons_spec.rb b/ee/spec/models/gitlab_subscriptions/trials/add_ons_spec.rb
deleted file mode 100644
index 45c2eb69cba8..000000000000
--- a/ee/spec/models/gitlab_subscriptions/trials/add_ons_spec.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe GitlabSubscriptions::Trials::AddOns, feature_category: :subscription_management do
-  describe '.eligible_namespace?' do
-    context 'when namespace_id is blank' do
-      it 'returns true for nil' do
-        expect(described_class.eligible_namespace?(nil, [])).to be(true)
-      end
-
-      it 'returns true for empty string' do
-        expect(described_class.eligible_namespace?('', [])).to be(true)
-      end
-    end
-
-    context 'when namespace_id is present' do
-      let_it_be(:namespace) { create(:group) }
-      let(:eligible_namespaces) { Namespace.id_in(namespace.id) }
-
-      it 'returns true for an eligible namespace' do
-        expect(described_class.eligible_namespace?(namespace.id.to_s, eligible_namespaces)).to be(true)
-      end
-
-      it 'returns false for an in-eligible namespace' do
-        expect(described_class.eligible_namespace?(non_existing_record_id.to_s, eligible_namespaces)).to be(false)
-      end
-    end
-  end
-end
diff --git a/ee/spec/models/gitlab_subscriptions/trials_spec.rb b/ee/spec/models/gitlab_subscriptions/trials_spec.rb
index 73908fa50480..49c65f7c2044 100644
--- a/ee/spec/models/gitlab_subscriptions/trials_spec.rb
+++ b/ee/spec/models/gitlab_subscriptions/trials_spec.rb
@@ -25,6 +25,47 @@
     end
   end
 
+  describe '.eligible_namespace?' do
+    context 'when namespace_id is blank' do
+      it 'returns true for nil' do
+        expect(described_class.eligible_namespace?(nil, [])).to be(true)
+      end
+
+      it 'returns true for empty string' do
+        expect(described_class.eligible_namespace?('', [])).to be(true)
+      end
+    end
+
+    context 'when namespace_id is present' do
+      let_it_be(:namespace) { create(:group) }
+      let(:eligible_namespaces) { Namespace.id_in(namespace.id) }
+
+      it 'returns true for an eligible namespace' do
+        expect(described_class.eligible_namespace?(namespace.id.to_s, eligible_namespaces)).to be(true)
+      end
+
+      it 'returns false for an in-eligible namespace' do
+        expect(described_class.eligible_namespace?(non_existing_record_id.to_s, eligible_namespaces)).to be(false)
+      end
+    end
+  end
+
+  describe '.creating_group_trigger?' do
+    subject { described_class.creating_group_trigger?(namespace_id) }
+
+    where(:namespace_id, :expected_result) do
+      [
+        [0,   true],
+        [nil, false],
+        [1,   false]
+      ]
+    end
+
+    with_them do
+      it { is_expected.to be(expected_result) }
+    end
+  end
+
   describe '.namespace_eligible?', :saas do
     subject { described_class.namespace_eligible?(namespace) }
 
diff --git a/ee/spec/requests/gitlab_subscriptions/trials_controller_spec.rb b/ee/spec/requests/gitlab_subscriptions/trials_controller_spec.rb
index 1db3c729686e..2c6d36febecf 100644
--- a/ee/spec/requests/gitlab_subscriptions/trials_controller_spec.rb
+++ b/ee/spec/requests/gitlab_subscriptions/trials_controller_spec.rb
@@ -2,7 +2,7 @@
 
 require 'spec_helper'
 
-RSpec.describe GitlabSubscriptions::TrialsController, feature_category: :plan_provisioning do
+RSpec.describe GitlabSubscriptions::TrialsController, :saas, feature_category: :plan_provisioning do
   let_it_be(:user, reload: true) { create(:user) }
   let(:glm_params) { { glm_source: '_glm_source_', glm_content: '_glm_content_' } }
   let(:subscriptions_trials_enabled) { true }
@@ -11,8 +11,29 @@
     stub_saas_features(subscriptions_trials: subscriptions_trials_enabled, marketing_google_tag_manager: false)
   end
 
+  shared_examples 'namespace_id is passed' do
+    context 'when namespace_id is 0' do
+      let(:namespace_id) { { namespace_id: 0 } }
+
+      it { is_expected.to have_gitlab_http_status(:ok) }
+    end
+
+    context 'for an ineligible group due to subscription level' do
+      let(:namespace_id) { { namespace_id: create(:group_with_plan, plan: :ultimate_plan, owners: user) } }
+
+      it { is_expected.to have_gitlab_http_status(:forbidden) }
+    end
+
+    context 'for an ineligible group due to user permissions' do
+      let(:namespace_id) { { namespace_id: create(:group) } }
+
+      it { is_expected.to have_gitlab_http_status(:forbidden) }
+    end
+  end
+
   describe 'GET new' do
-    let(:base_params) { glm_params }
+    let(:namespace_id) { {} }
+    let(:base_params) { glm_params.merge(namespace_id) }
 
     subject(:get_new) do
       get new_trial_path, params: base_params
@@ -30,23 +51,13 @@
 
       it { is_expected.to have_gitlab_http_status(:ok) }
 
-      context 'with eligible_namespaces' do
-        context 'and there are no eligible namespaces' do
-          it 'is empty' do
-            get_new
-
-            expect(assigns(:eligible_namespaces)).to be_empty
-          end
-        end
+      it_behaves_like 'namespace_id is passed'
 
-        context 'and there are eligible namespaces' do
-          it 'assigns eligible_namespaces with namespace' do
-            namespace = create(:group, owners: user)
-
-            get_new
+      context 'when there are no eligible namespaces' do
+        it 'is empty' do
+          get_new
 
-            expect(assigns(:eligible_namespaces)).to match_array([namespace])
-          end
+          expect(assigns(:eligible_namespaces)).to be_empty
         end
       end
 
@@ -69,13 +80,19 @@
       context 'when on the trial step' do
         let(:base_params) { { step: 'trial' } }
 
+        before_all do
+          create(:group, owners: user)
+        end
+
         it { is_expected.to render_select_namespace }
       end
     end
   end
 
   describe 'POST create' do
+    let_it_be(:namespace, reload: true) { create(:group_with_plan, plan: :free_plan, owners: user) }
     let(:step) { 'lead' }
+    let(:namespace_id) { { namespace_id: namespace.id.to_s } }
     let(:lead_params) do
       {
         company_name: '_company_name_',
@@ -91,10 +108,9 @@
 
     let(:trial_params) do
       {
-        namespace_id: non_existing_record_id.to_s,
         trial_entity: '_trial_entity_',
         organization_id: anything
-      }.with_indifferent_access
+      }.merge(namespace_id).with_indifferent_access
     end
 
     let(:base_params) { lead_params.merge(trial_params).merge(glm_params).merge(step: step) }
@@ -129,6 +145,15 @@
         login_as(user)
       end
 
+      it_behaves_like 'namespace_id is passed' do
+        before do
+          allow_next_instance_of(GitlabSubscriptions::Trials::CreateService) do |instance|
+            response = ServiceResponse.error(message: '_message_', payload: { namespace: namespace })
+            allow(instance).to receive(:execute).and_return(response)
+          end
+        end
+      end
+
       context 'when user is then banned' do
         before do
           user.ban!
@@ -142,8 +167,7 @@
         end
       end
 
-      context 'when successful', :saas do
-        let_it_be(:namespace, reload: true) { create(:group_with_plan, plan: :free_plan) }
+      context 'when successful' do
         let_it_be(:add_on) { create(:gitlab_subscription_add_on, :duo_enterprise) }
         let_it_be(:ultimate_trial_plan) { create(:ultimate_trial_plan) }
 
@@ -172,7 +196,7 @@
         end
 
         context 'when the namespace applying is on the premium plan' do
-          let_it_be(:namespace) { create(:group_with_plan, plan: :premium_plan) }
+          let_it_be(:namespace) { create(:group_with_plan, plan: :premium_plan, owners: user) }
           let_it_be(:ultimate_trial_paid_customer_plan) { create(:ultimate_trial_paid_customer_plan) }
 
           context 'when add_on_purchase exists' do
@@ -354,31 +378,10 @@ def update_with_applied_trials(namespace)
 
             expect(response).to redirect_to(new_trial_path(payload[:trial_selection_params]))
           end
-
-          context 'with eligible_namespaces' do
-            context 'and there are no eligible namespaces' do
-              it 'is empty' do
-                post_create
-
-                expect(assigns(:eligible_namespaces)).to be_empty
-              end
-            end
-
-            context 'and there are eligible namespaces' do
-              it 'assigns eligible_namespaces with namespace' do
-                namespace = create(:group, owners: user)
-
-                post_create
-
-                expect(assigns(:eligible_namespaces)).to match_array([namespace])
-              end
-            end
-          end
         end
 
         context 'with namespace creation failure' do
           let(:failure_reason) { :namespace_create_failed }
-          let(:namespace) { build_stubbed(:namespace) }
           let(:payload) { { namespace_id: namespace.id } }
 
           it 'renders the select namespace form again with namespace creation errors only' do
@@ -391,7 +394,6 @@ def update_with_applied_trials(namespace)
 
         context 'with trial failure' do
           let(:failure_reason) { :trial_failed }
-          let(:namespace) { build_stubbed(:namespace) }
           let(:payload) { { namespace_id: namespace.id } }
 
           it 'renders the select namespace form again with trial creation errors only' do
@@ -403,7 +405,6 @@ def update_with_applied_trials(namespace)
 
         context 'with random failure' do
           let(:failure_reason) { :random_error }
-          let(:namespace) { build_stubbed(:namespace) }
           let(:payload) { { namespace_id: namespace.id } }
 
           it { is_expected.to render_select_namespace }
@@ -429,7 +430,7 @@ def expect_create_failure(reason, payload = {})
   RSpec::Matchers.define :render_select_namespace do
     match do |response|
       expect(response).to have_gitlab_http_status(:ok)
-      expect(response.body).to include(_('Apply your trial to a new group'))
+      expect(response.body).to include(_('Apply your trial to a new or existing group'))
       expect(response.body).to include(_('Activate my trial'))
     end
   end
-- 
GitLab