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