ee/spec/helpers/admin/application_settings_helper_spec.rb (389 lines of code) (raw):

# frozen_string_literal: true require "spec_helper" RSpec.describe Admin::ApplicationSettingsHelper, feature_category: :ai_abstraction_layer do using RSpec::Parameterized::TableSyntax let(:duo_availability) { :default_off } let(:instance_level_ai_beta_features_enabled) { false } let(:model_prompt_cache_enabled) { true } let(:disabled_direct_code_suggestions) { false } let(:enabled_expanded_logging) { true } let(:duo_chat_expiration_column) { 'created_at' } let(:duo_chat_expiration_days) { 25 } let(:code_suggestions_service) { instance_double(CloudConnector::AvailableServices) } before do stub_ee_application_setting(duo_availability: duo_availability) stub_ee_application_setting(instance_level_ai_beta_features_enabled: instance_level_ai_beta_features_enabled) stub_ee_application_setting(model_prompt_cache_enabled: model_prompt_cache_enabled) stub_ee_application_setting(enabled_expanded_logging: enabled_expanded_logging) stub_ee_application_setting(disabled_direct_code_suggestions: disabled_direct_code_suggestions) stub_ee_application_setting(duo_chat_expiration_column: duo_chat_expiration_column) stub_ee_application_setting(duo_chat_expiration_days: duo_chat_expiration_days) allow(CloudConnector::AvailableServices) .to receive(:find_by_name).with(:code_suggestions).and_return(code_suggestions_service) end describe 'AI-native features settings for Self-Managed instances' do describe '#admin_ai_configuration_settings_helper_data' do subject(:admin_ai_configuration_settings_helper_data) { helper.admin_ai_configuration_settings_helper_data } before do allow(helper).to receive(:ai_settings_helper_data).and_return({ base_data: 'data' }) end it 'returns the expected data' do expect(admin_ai_configuration_settings_helper_data).to include( on_general_settings_page: 'false', redirect_path: '/admin/gitlab_duo', base_data: 'data' ) end end describe '#ai_settings_helper_data' do using RSpec::Parameterized::TableSyntax subject { helper.ai_settings_helper_data } let(:service) { double('CodeSuggestionsService') } # rubocop:disable RSpec/VerifiedDoubles -- Stubbed to test purchases call let(:enterprise_service) { double('EnterpriseService') } # rubocop:disable RSpec/VerifiedDoubles -- Stubbed to test purchases call let(:ai_gateway_url) { "http://0.0.0.0:5052" } let(:duo_availability) { 'default_on' } let(:instance_level_ai_beta_features_enabled) { false } let(:model_prompt_cache_enabled) { 'true' } let(:enabled_expanded_logging) { false } let(:disabled_direct_code_suggestions) { false } let(:duo_chat_expiration_column) { 'created_at' } let(:duo_chat_expiration_days) { 30 } let(:duo_core_features_enabled) { false } where( :terms_accepted, :purchased, :ultimate, :premium, :duo_ent_purchased, :expected_duo_pro_visible_value, :expected_experiments_visible_value, :expected_can_manage_self_hosted_models, :expected_duo_core_features_enabled ) do true | true | true | false | true | 'true' | 'true' | 'true' | 'true' true | true | false | true | true | 'true' | 'true' | 'true' | 'true' true | true | true | false | false | 'true' | 'true' | 'false' | 'true' true | true | false | true | false | 'true' | 'true' | 'false' | 'false' true | true | false | false | true | 'true' | 'true' | 'false' | 'false' false | false | false | false | false | 'false' | 'false' | 'false' | 'false' true | nil | true | false | false | '' | 'false' | 'false' | 'false' end with_them do let(:expected_settings_helper_data) do { duo_availability: duo_availability.to_s, experiment_features_enabled: instance_level_ai_beta_features_enabled.to_s, prompt_cache_enabled: model_prompt_cache_enabled, are_experiment_settings_allowed: expected_experiments_visible_value.to_s, are_prompt_cache_settings_allowed: 'true', enabled_expanded_logging: enabled_expanded_logging.to_s, disabled_direct_connection_method: disabled_direct_code_suggestions.to_s, beta_self_hosted_models_enabled: terms_accepted.to_s, toggle_beta_models_path: admin_ai_duo_self_hosted_toggle_beta_models_path, duo_pro_visible: expected_duo_pro_visible_value, can_manage_self_hosted_models: expected_can_manage_self_hosted_models.to_s, ai_gateway_url: ai_gateway_url, duo_chat_expiration_column: duo_chat_expiration_column, duo_chat_expiration_days: duo_chat_expiration_days.to_s, duo_core_features_enabled: expected_duo_core_features_enabled.to_s, is_duo_base_access_allowed: 'true', duo_pro_or_duo_enterprise_tier: nil, should_show_duo_availability: 'false' } end before do allow(::Gitlab::CurrentSettings) .to receive(:disabled_direct_code_suggestions) .and_return(disabled_direct_code_suggestions) allow(helper).to receive_messages( experiments_settings_allowed?: expected_experiments_visible_value == 'true', prompt_cache_settings_allowed?: true, duo_availability: duo_availability, instance_level_ai_beta_features_enabled: instance_level_ai_beta_features_enabled, enabled_expanded_logging: enabled_expanded_logging, current_application_settings: double( # rubocop:disable RSpec/VerifiedDoubles -- Stubbed to test expiration call duo_chat_expiration_column: duo_chat_expiration_column, duo_chat_expiration_days: duo_chat_expiration_days, model_prompt_cache_enabled: model_prompt_cache_enabled ) ) allow(::Ai::TestingTermsAcceptance).to receive(:has_accepted?).and_return(terms_accepted) allow(License).to receive_message_chain(:current, :ultimate?).and_return(ultimate) allow(License).to receive_message_chain(:current, :premium?).and_return(premium) allow(::GitlabSubscriptions::AddOnPurchase) .to receive_message_chain(:for_self_managed, :for_duo_enterprise, :active, :exists?) .and_return(duo_ent_purchased) allow(::GitlabSubscriptions::AddOnPurchase) .to receive_message_chain(:for_self_managed, :for_duo_pro_or_duo_enterprise, :active, :first) allow(::Ai::Setting).to receive_message_chain(:instance, :ai_gateway_url).and_return(ai_gateway_url) allow(::Ai::Setting).to receive_message_chain(:instance, :enabled_instance_verbose_ai_logs) .and_return(enabled_expanded_logging) setup_cloud_connector_services(purchased) allow(::Ai::Setting).to receive_message_chain(:instance, :duo_core_features_enabled?) .and_return expected_duo_core_features_enabled end it 'returns the expected data' do is_expected.to eq(expected_settings_helper_data) end context 'with feature flag allow_duo_base_access set to false' do before do stub_feature_flags(allow_duo_base_access: false) end it 'sets is_duo_base_access_allowed to false' do expect(helper.ai_settings_helper_data).to include(is_duo_base_access_allowed: 'false') end end end def setup_cloud_connector_services(purchased) if purchased.nil? allow(CloudConnector::AvailableServices) .to receive(:find_by_name).with(:code_suggestions).and_return(nil) else allow(CloudConnector::AvailableServices) .to receive(:find_by_name).with(:code_suggestions).and_return(service) allow(service).to receive(:purchased?).and_return(purchased) end allow(CloudConnector::AvailableServices) .to receive(:find_by_name).with(:anthropic_proxy).and_return(enterprise_service) allow(enterprise_service).to receive(:purchased?).and_return(true) end end end describe '#ai_settings_helper_data[:duo_pro_or_duo_enterprise_tier]' do subject { helper.ai_settings_helper_data[:duo_pro_or_duo_enterprise_tier] } before do allow(CloudConnector::AvailableServices).to receive(:find_by_name).with(:code_suggestions) allow(CloudConnector::AvailableServices).to receive(:find_by_name).with(:anthropic_proxy) allow(GitlabSubscriptions::Trials::DuoProOrDuoEnterprise) .to receive(:any_add_on_purchase) .with(nil) .and_return(duo_pro_or_duo_enterprise_add_on_purchase) end context 'with Duo Pro' do let(:duo_pro_or_duo_enterprise_add_on_purchase) do build(:gitlab_subscription_add_on_purchase, :duo_pro, :self_managed, :active) end it { is_expected.to eq 'CODE_SUGGESTIONS' } end context 'with Duo Enterprise' do let(:duo_pro_or_duo_enterprise_add_on_purchase) do build(:gitlab_subscription_add_on_purchase, :duo_enterprise, :self_managed, :active) end it { is_expected.to eq 'DUO_ENTERPRISE' } end end describe '#ai_settings_helper_data[:should_show_duo_availability]' do subject { helper.ai_settings_helper_data[:should_show_duo_availability] } before do allow(CloudConnector::AvailableServices).to receive(:find_by_name).with(:code_suggestions) allow(CloudConnector::AvailableServices).to receive(:find_by_name).with(:anthropic_proxy) allow(GitlabSubscriptions::Trials::DuoProOrDuoEnterprise) .to receive(:any_add_on_purchased_or_trial?) .with(nil) .and_return(duo_pro_or_duo_enterprise_add_on_purchase.active?) end context 'with active Duo add-on' do let(:duo_pro_or_duo_enterprise_add_on_purchase) do build(:gitlab_subscription_add_on_purchase, :duo_pro, :active) end it { is_expected.to eq 'true' } end context 'with expired Duo add-on' do let(:duo_pro_or_duo_enterprise_add_on_purchase) do build(:gitlab_subscription_add_on_purchase, :duo_enterprise, :expired) end it { is_expected.to eq 'false' } end end describe '#admin_display_duo_addon_settings?' do subject(:display_duo_pro_settings) { helper.admin_display_duo_addon_settings? } let(:duo_add_on_purchased) { false } before do allow(GitlabSubscriptions::AddOnPurchase) .to receive_message_chain(:for_self_managed, :for_duo_core_pro_or_enterprise, :active, :any?) .and_return(duo_add_on_purchased) end context 'when a self-managed Duo Core, Duo Pro or Duo Enterprise purchase exists' do let(:duo_add_on_purchased) { true } it { is_expected.to be true } end context 'when no self-managed Duo Core, Duo Pro or Duo Enterprise purchase exists' do let(:duo_add_on_purchased) { false } it { is_expected.to be false } end end describe '#admin_duo_home_app_data' do let(:starts_at) { Date.current } let(:expires_at) { Date.current + 1.year } let(:license) { build(:gitlab_license, starts_at: starts_at, expires_at: expires_at) } let(:subscription_name) { 'Test Subscription Name' } let(:amazon_q_available) { false } let(:duo_workflow_enabled) { false } let(:duo_workflow_service_account) { nil } let(:is_saas) { false } let(:duo_core_features_enabled) { true } before do allow(License).to receive(:current).and_return(license) allow(license).to receive(:ultimate?).and_return(true) allow(::Ai::AmazonQ).to receive(:feature_available?).and_return(amazon_q_available) allow(license).to receive_messages( subscription_name: subscription_name, subscription_start_date: starts_at, subscription_end_date: expires_at ) allow(helper).to receive_messages( admin_gitlab_duo_seat_utilization_index_path: '/admin/gitlab_duo/seat_utilization', admin_gitlab_duo_configuration_index_path: '/admin/gitlab_duo/configuration', admin_gitlab_duo_path: '/admin/gitlab_duo', admin_ai_duo_workflow_settings_path: '/admin/ai/duo_workflow/settings', disconnect_admin_ai_duo_workflow_settings_path: '/admin/ai/duo_workflow/settings/disconnect', duo_pro_bulk_user_assignment_available?: true, duo_availability: 'default_off', instance_level_ai_beta_features_enabled: true, experiments_settings_allowed?: true, duo_workflow_service_account: duo_workflow_service_account ) allow(helper).to receive(:add_duo_pro_seats_url).with(subscription_name).and_return('https://customers.staging.gitlab.com/gitlab/subscriptions/A-S00613274/duo_pro_seats') allow(Gitlab::CurrentSettings).to receive_message_chain(:current, :disabled_direct_code_suggestions).and_return(false) allow(::Ai::TestingTermsAcceptance).to receive(:has_accepted?).and_return(true) allow(::Ai::DuoWorkflow).to receive(:available?).and_return(duo_workflow_enabled) allow(::Gitlab).to receive(:com?).and_return(is_saas) allow(::Ai::Setting).to receive_message_chain(:instance, :duo_core_features_enabled?) .and_return(duo_core_features_enabled) allow(::Ai::Setting).to receive_message_chain(:instance, :ai_gateway_url) .and_return('http://0.0.0.0:5052') end it 'returns a hash with all required keys and correct values' do expect(helper.admin_duo_home_app_data).to eq({ ai_gateway_url: 'http://0.0.0.0:5052', duo_seat_utilization_path: '/admin/gitlab_duo/seat_utilization', duo_configuration_path: '/admin/gitlab_duo/configuration', enabled_expanded_logging: 'true', add_duo_pro_seats_url: 'https://customers.staging.gitlab.com/gitlab/subscriptions/A-S00613274/duo_pro_seats', subscription_name: 'Test Subscription Name', is_bulk_add_on_assignment_enabled: 'true', is_duo_base_access_allowed: 'true', subscription_start_date: starts_at, subscription_end_date: expires_at, duo_availability: 'default_off', direct_code_suggestions_enabled: 'true', experiment_features_enabled: 'true', prompt_cache_enabled: 'true', beta_self_hosted_models_enabled: 'true', are_experiment_settings_allowed: 'true', are_prompt_cache_settings_allowed: 'true', duo_workflow_enabled: 'false', duo_workflow_service_account: nil, is_saas: 'false', duo_workflow_settings_path: '/admin/ai/duo_workflow/settings', duo_workflow_disable_path: '/admin/ai/duo_workflow/settings/disconnect', duo_self_hosted_path: '/admin/ai/duo_self_hosted', redirect_path: '/admin/gitlab_duo', can_manage_self_hosted_models: 'false', duo_add_on_start_date: nil, duo_add_on_end_date: nil, are_duo_core_features_enabled: 'true' }) end context 'with disabled duo_core_features_enabled' do let(:duo_core_features_enabled) { false } it 'sets Duo Core flag to false' do expect(helper.admin_duo_home_app_data).to include(are_duo_core_features_enabled: 'false') end end context 'when the instance is SaaS' do let(:is_saas) { true } it 'sets is_saas to true' do expect(helper.admin_duo_home_app_data[:is_saas]).to eq('true') end end context 'when the instance is Gitlab Dedicated' do before do allow(Gitlab::CurrentSettings).to receive(:gitlab_dedicated_instance?).and_return(true) end it 'sets can_manage_self_hosted_models to false' do expect(helper.admin_duo_home_app_data[:can_manage_self_hosted_models]).to eq('false') end end context 'with feature flag allow_duo_base_access set to false' do before do stub_feature_flags(allow_duo_base_access: false) end it 'sets is_duo_base_access_allowed to false' do expect(helper.admin_duo_home_app_data).to include(is_duo_base_access_allowed: 'false') end end context 'when the instance has a Duo purchase' do let(:duo_start_date) { Date.current - 1.month } let(:duo_end_date) { Date.current + 11.months } let(:duo_purchase) do build( :gitlab_subscription_add_on_purchase, :self_managed, :duo_enterprise, started_at: duo_start_date, expires_on: duo_end_date ) end before do allow(GitlabSubscriptions::AddOnPurchase) .to receive_message_chain(:for_self_managed, :for_duo_pro_or_duo_enterprise, :last) .and_return(duo_purchase) allow(::GitlabSubscriptions::AddOnPurchase) .to receive_message_chain(:for_self_managed, :for_duo_enterprise, :active, :exists?) .and_return(true) end it 'includes the correct values' do result = helper.admin_duo_home_app_data expect(result).to include(duo_add_on_start_date: duo_start_date, duo_add_on_end_date: duo_end_date) end end context 'when direct connections is disabled' do before do allow(Gitlab::CurrentSettings).to receive(:disabled_direct_code_suggestions).and_return(true) end it 'returns the correct value' do expect(helper.admin_duo_home_app_data[:direct_code_suggestions_enabled]).to eq 'false' end end context 'when Amazon Q is available' do let(:amazon_q_available) { true } where(:auto_review_enabled, :amazon_q_ready) do false | true true | false end with_them do let(:integration) { build_stubbed(:amazon_q_integration, auto_review_enabled: auto_review_enabled) } it 'includes the related data' do allow(::Integrations::AmazonQ).to receive(:for_instance).and_return([integration]) allow(::Ai::Setting.instance).to receive(:amazon_q_ready).and_return(amazon_q_ready) expect(helper.admin_duo_home_app_data).to include( amazon_q_ready: amazon_q_ready.to_s, amazon_q_auto_review_enabled: auto_review_enabled.to_s, amazon_q_configuration_path: '/admin/application_settings/integrations/amazon_q/edit' ) end end end context 'when duo workflow service account user exists' do let(:duo_workflow_enabled) { true } let(:service_account) { build_stubbed(:user, id: 123, username: 'duo_service', name: 'Duo Service') } let(:user_data) { { id: 123, username: 'duo_service', name: 'Duo Service', avatar_url: 'avatar.png' } } let(:ai_setting) do instance_double(Ai::Setting, duo_core_features_enabled?: true, ai_gateway_url: 'http://0.0.0.0:5052') end before do allow(helper).to receive(:duo_workflow_service_account).and_call_original allow(license).to receive(:feature_available?).and_return(false) allow(Ai::Setting).to receive(:instance).and_return(ai_setting) allow(ai_setting).to receive(:duo_workflow_service_account_user).and_return(service_account) allow(service_account).to receive(:slice).with(:id, :username, :name, :avatar_url).and_return(user_data) end it 'includes the sliced and JSON-converted service account data' do result = helper.admin_duo_home_app_data expect(result[:duo_workflow_enabled]).to eq('true') expect(result[:duo_workflow_service_account]).to eq( '{"id":123,"username":"duo_service","name":"Duo Service","avatar_url":"avatar.png"}' ) end end end end