From 5b8625a1609c755307374189d7c20e955f424252 Mon Sep 17 00:00:00 2001
From: Lindsey Shelton <lshelton@gitlab.com>
Date: Mon, 7 Oct 2024 16:33:06 +0000
Subject: [PATCH] Fix for Personal Projects Settings bug

Update namespaces_helper to use project.parent
Update associated specs

Changelog: fixed
---
 app/helpers/namespaces_helper.rb       |  26 +++---
 app/helpers/projects_helper.rb         |   2 +-
 doc/development/cascading_settings.md  |   2 +-
 spec/helpers/namespaces_helper_spec.rb | 124 ++++++++++++++++---------
 4 files changed, 93 insertions(+), 61 deletions(-)

diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb
index 90f5a4ede7c8a..44db2fc336937 100644
--- a/app/helpers/namespaces_helper.rb
+++ b/app/helpers/namespaces_helper.rb
@@ -56,26 +56,22 @@ def cascading_namespace_settings_tooltip_raw_data(attribute, group, settings_pat
   def project_cascading_namespace_settings_tooltip_data(attribute, project, settings_path_helper)
     return unless attribute && project && settings_path_helper
 
-    data = cascading_namespace_settings_tooltip_raw_data(attribute, project.group, settings_path_helper)
-    return {} if data.nil?
+    data = cascading_namespace_settings_tooltip_raw_data(attribute, project.parent, settings_path_helper)
+    return data if data[:locked_by_ancestor]
 
-    update_project_data_with_lock_info(data, "#{attribute}_locked?", project)
+    data[:locked_by_application_setting] = check_project_lock(project, "#{attribute}_locked_by_application_setting?")
+    locked_by_ancestor = check_project_lock(project, "#{attribute}_locked_by_ancestor?")
+    locked_by_project = check_project_lock(project, "#{attribute}_locked?")
 
-    Gitlab::Json.dump(data)
-    data.to_json
-  end
-
-  def update_project_data_with_lock_info(data, attribute, project)
-    return if data["locked_by_ancestor"]
+    return data unless locked_by_ancestor || locked_by_project
 
-    locked_by_group = check_project_lock(project, attribute)
-    return unless locked_by_group
-
-    data[:locked_by_ancestor] = locked_by_group
+    data[:locked_by_ancestor] = true
     data[:ancestor_namespace] = {
-      full_name: project.group.name,
-      path: edit_group_path(project.group)
+      full_name: project.parent.name,
+      path: Gitlab::UrlBuilder.build(project.parent, only_path: true)
     }
+
+    data
   end
 
   def cascading_namespace_setting_locked?(attribute, group, **args)
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 2b33bf5e9f2c4..03c468a45e1dc 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -922,7 +922,7 @@ def confirm_reduce_visibility_message(project)
 
   def project_permissions_data(project, target_form_id = nil)
     data = visibility_confirm_modal_data(project, target_form_id)
-    cascading_settings_data = project_cascading_namespace_settings_tooltip_data(:duo_features_enabled, project, method(:edit_group_path))
+    cascading_settings_data = project_cascading_namespace_settings_tooltip_data(:duo_features_enabled, project, method(:edit_group_path)).to_json
     data.merge!(
       {
         cascading_settings_data: cascading_settings_data
diff --git a/doc/development/cascading_settings.md b/doc/development/cascading_settings.md
index fa5c7311596c7..4009785459097 100644
--- a/doc/development/cascading_settings.md
+++ b/doc/development/cascading_settings.md
@@ -248,7 +248,7 @@ initCascadingSettingsLockTooltips();
 
  ```ruby
  # Example call from your Ruby helper  method for projects
-cascading_settings_data = project_cascading_namespace_settings_tooltip_data(:duo_features_enabled, project, method(:edit_group_path))
+cascading_settings_data = project_cascading_namespace_settings_tooltip_data(:duo_features_enabled, project, method(:edit_group_path)).to_json
  ```
 
 1. From your Vue's `index.js` file, be sure to convert the data into JSON and camel case format. This will make it easier to use in Vue.
diff --git a/spec/helpers/namespaces_helper_spec.rb b/spec/helpers/namespaces_helper_spec.rb
index 96fff67bcd312..04868693587e5 100644
--- a/spec/helpers/namespaces_helper_spec.rb
+++ b/spec/helpers/namespaces_helper_spec.rb
@@ -37,14 +37,20 @@
     )
   end
 
+  let!(:project1) { build(:project, namespace: subgroup1) }
+  let!(:project2) do
+    user.create_namespace!(path: user.username, name: user.name) unless user.namespace
+    build(:project, namespace: user.namespace)
+  end
+
+  let_it_be(:attribute) { :math_rendering_limits_enabled }
+
   before do
     admin_group.add_owner(admin)
     user_group.add_owner(user)
   end
 
   describe '#check_group_lock' do
-    attribute = :math_rendering_limits_enabled
-
     context 'when the method exists on namespace_settings' do
       it 'calls the method on namespace_settings' do
         expect(subgroup1.namespace_settings).to receive(attribute).and_return(true)
@@ -62,8 +68,6 @@
   describe '#check_project_lock' do
     let(:project) { build(:project, group: subgroup1) }
 
-    attribute = :math_rendering_limits_enabled
-
     it 'returns true when the method exists and returns true' do
       allow(project.project_setting).to receive(attribute).and_return(true)
       expect(helper.check_project_lock(project, attribute)).to be true
@@ -75,8 +79,6 @@
   end
 
   describe '#cascading_namespace_settings_tooltip_data' do
-    attribute = :math_rendering_limits_enabled
-
     it 'returns tooltip data with testid' do
       allow(helper).to receive(:cascading_namespace_settings_tooltip_raw_data).and_return({ key: 'value' })
       result = helper.cascading_namespace_settings_tooltip_data(attribute, subgroup1, -> {})
@@ -86,8 +88,6 @@
   end
 
   describe '#cascading_namespace_settings_tooltip_raw_data' do
-    attribute = :math_rendering_limits_enabled
-
     subject do
       helper.cascading_namespace_settings_tooltip_data(
         attribute,
@@ -137,61 +137,97 @@
   end
 
   describe '#project_cascading_namespace_settings_tooltip_data' do
-    let(:project) { build(:project, group: subgroup1) }
+    using RSpec::Parameterized::TableSyntax
 
-    attribute = :math_rendering_limits_enabled
-    settings_path_helper = ->(locked_ancestor) { edit_group_path(locked_ancestor) }
-    subject { helper.project_cascading_namespace_settings_tooltip_data(attribute, project, settings_path_helper) }
+    let(:settings_path_helper) { ->(locked_ancestor) { edit_group_path(locked_ancestor) } }
 
-    context 'when not locked by ancestor' do
-      before do
-        allow(helper).to receive(:cascading_namespace_settings_tooltip_data)
-        .and_return(tooltip_data: { locked_by_ancestor: false }.to_json)
+    subject do
+      helper.project_cascading_namespace_settings_tooltip_data(attribute, project, settings_path_helper)
+    end
+
+    shared_examples 'returns correct data' do
+      context 'when attribute is nil' do
+        let(:attribute) { nil }
+
+        it { is_expected.to be nil }
       end
 
-      context 'when locked by project group' do
-        before do
-          allow(project.project_setting).to receive("#{attribute}_locked?").and_return(true)
-        end
+      context 'when project is nil' do
+        let(:project) { nil }
 
-        it 'returns JSON with locked_by_ancestor true and ancestor_namespace object' do
-          result = Gitlab::Json.parse(subject)
-          expect(result['locked_by_ancestor']).to be true
-          expect(result['ancestor_namespace']).to eq({
-            'full_name' => project.group.name,
-            'path' => edit_group_path(project.group)
-          })
-        end
+        it { is_expected.to be nil }
       end
 
-      context 'when not locked by project group' do
+      context 'when settings_path_helper is nil' do
+        let(:settings_path_helper) { nil }
+
+        it { is_expected.to be nil }
+      end
+
+      where(:locked_by_ancestor, :locked_by_application_setting, :locked_by_project, :expected_result) do
+        false | false | false | { locked_by_application_setting: false, locked_by_ancestor: false }
+        false | true  | false | { locked_by_application_setting: true, locked_by_ancestor: false }
+        true  | false | false | { locked_by_application_setting: false, locked_by_ancestor: true, ancestor_namespace: { full_name: String, path: String } }
+        false | false | true  | { locked_by_application_setting: false, locked_by_ancestor: true, ancestor_namespace: { full_name: String, path: String } }
+      end
+
+      with_them do
         before do
-          allow(project.project_setting).to receive("#{attribute}_locked").and_return(false)
+          allow(helper).to receive(:check_project_lock)
+            .with(project, "#{attribute}_locked_by_application_setting?")
+            .and_return(locked_by_application_setting)
+
+          allow(helper).to receive(:check_project_lock)
+            .with(project, "#{attribute}_locked_by_ancestor?")
+            .and_return(locked_by_ancestor)
+
+          allow(helper).to receive(:check_project_lock)
+            .with(project, "#{attribute}_locked?")
+            .and_return(locked_by_project)
+        end
+
+        it 'returns the expected result' do
+          is_expected.to include(expected_result)
+
+          if expected_result[:ancestor_namespace]
+            expect(subject[:ancestor_namespace][:full_name]).to eq(project.parent.name)
+            expect(subject[:ancestor_namespace][:path]).to eq(Gitlab::UrlBuilder.build(project.parent, only_path: true))
+          end
         end
 
-        it 'returns JSON without changing locked_by_ancestor' do
-          expect(Gitlab::Json.parse(subject)['locked_by_ancestor']).to be false
-          expect(Gitlab::Json.parse(subject)).not_to have_key('ancestor_namespace')
+        context 'when data is already locked by ancestor' do
+          before do
+            allow(helper).to receive(:check_group_lock)
+              .with(project.namespace, "#{attribute}_locked_by_ancestor?")
+              .and_return(true)
+            allow(helper).to receive(:check_group_lock)
+              .with(project.namespace, "#{attribute}_locked_by_application_setting?")
+              .and_return(false)
+          end
+
+          it 'returns early without modifying data' do
+            expect(helper).not_to receive(:check_project_lock)
+
+            is_expected.to eq({ locked_by_ancestor: true, locked_by_application_setting: false })
+          end
         end
       end
     end
 
-    context 'when locked by ancestor' do
-      before do
-        allow(helper).to receive(:cascading_namespace_settings_tooltip_raw_data)
-          .and_return({ locked_by_ancestor: true })
-      end
+    context 'when project parent is a group' do
+      let(:project) { project1 }
 
-      it 'returns JSON without changing locked_by_ancestor' do
-        expect(Gitlab::Json.parse(subject)['locked_by_ancestor']).to be true
-        expect(Gitlab::Json.parse(subject)).not_to have_key('ancestor_namespace')
-      end
+      it_behaves_like 'returns correct data'
+    end
+
+    context 'when project parent is a user namespace' do
+      let(:project) { project2 }
+
+      it_behaves_like 'returns correct data'
     end
   end
 
   describe '#cascading_namespace_setting_locked?' do
-    let(:attribute) { :math_rendering_limits_enabled }
-
     context 'when `group` argument is `nil`' do
       it 'returns `false`' do
         expect(helper.cascading_namespace_setting_locked?(attribute, nil)).to eq(false)
-- 
GitLab