diff --git a/app/controllers/groups/observability/o11y_service_settings_controller.rb b/app/controllers/groups/observability/o11y_service_settings_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..631e03a4e925196bac14db836c0fd3a34eb1b5a4 --- /dev/null +++ b/app/controllers/groups/observability/o11y_service_settings_controller.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +module Groups + module Observability + class O11yServiceSettingsController < Groups::ApplicationController + include Gitlab::Utils::StrongMemoize + + before_action :authenticate_user! + before_action :authorize_o11y_settings_access! + + feature_category :observability + urgency :low + + def update + if ::Observability::GroupO11ySettingsUpdateService.new(settings).execute(settings_params) + redirect_to edit_group_observability_o11y_service_settings_path(@group), + notice: _('Observability service settings updated successfully.') + else + render :edit + end + end + + def edit + settings + end + + def destroy + if settings.new_record? || settings.destroy + redirect_to edit_group_observability_o11y_service_settings_path(@group), + notice: _('Observability service settings deleted successfully.'), + status: :see_other + else + redirect_to edit_group_observability_o11y_service_settings_path(@group), + alert: _('Failed to delete observability service settings.'), + status: :see_other + end + end + + private + + def authorize_o11y_settings_access! + render_404 unless ::Feature.enabled?(:o11y_settings_access, current_user) + end + + def settings_params + params.require(:observability_group_o11y_setting).permit( + :o11y_service_url, + :o11y_service_user_email, + :o11y_service_password, + :o11y_service_post_message_encryption_key + ) + end + + def settings + group.observability_group_o11y_setting || group.build_observability_group_o11y_setting + end + strong_memoize_attr :settings + end + end +end diff --git a/app/services/observability/group_o11y_settings_update_service.rb b/app/services/observability/group_o11y_settings_update_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..a7f4b347acddaab88b4cb01dbc53cb3d66e53359 --- /dev/null +++ b/app/services/observability/group_o11y_settings_update_service.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Observability + class GroupO11ySettingsUpdateService < BaseService + def initialize(settings) + @settings = settings + end + + def execute(settings_params) + settings.update(filter_blank_params(settings_params)) + end + + private + + attr_reader :settings + + def filter_blank_params(params) + params.reject { |_key, value| value.blank? } + end + end +end diff --git a/app/views/groups/observability/o11y_service_settings/edit.html.haml b/app/views/groups/observability/o11y_service_settings/edit.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..4b36cef5c71585be26dbff4c33b48e65ebd6abc0 --- /dev/null +++ b/app/views/groups/observability/o11y_service_settings/edit.html.haml @@ -0,0 +1,61 @@ +- breadcrumb_title _("O11y Service Settings") +- page_title _("O11y Service Settings") +- @force_desktop_expanded_sidebar = true + +%h1.gl-sr-only= @breadcrumb_title + += render ::Layouts::SettingsBlockComponent.new(_('Observability Service Configuration'), + id: 'js-o11y-service-settings', + testid: 'o11y-service-settings', + expanded: true) do |c| + - c.with_description do + = _('Configure your observability service connection settings.') + - c.with_body do + = gitlab_ui_form_for @settings, url: group_observability_o11y_service_settings_path(@group), method: :put, html: { class: 'js-o11y-service-settings-form' }, authenticity_token: true do |f| + %input{ type: 'hidden', name: 'update_section', value: 'js-o11y-service-settings' } + = form_errors(@settings) + + .row + .form-group.col-md-6 + = f.label :o11y_service_url, _('Service URL'), class: 'label-bold' + = f.text_field :o11y_service_url, class: 'form-control', placeholder: 'https://your-o11y-instance.com', data: { testid: 'o11y-service-url-field' } + %span.form-text.gl-text-subtle + = _('The URL of your observability service instance.') + + .form-group.col-md-6 + = f.label :o11y_service_user_email, _('User Email'), class: 'label-bold' + = f.email_field :o11y_service_user_email, class: 'form-control', placeholder: 'user@example.com', data: { testid: 'o11y-service-user-email-field' } + %span.form-text.gl-text-subtle + = _('Email address for authentication with the observability service.') + + .row + .form-group.col-md-6 + = f.label :o11y_service_password, _('Password'), class: 'label-bold' + = f.password_field :o11y_service_password, class: 'form-control', placeholder: (@settings.persisted? ? '••••••••' : nil), data: { testid: 'o11y-service-password-field' } + %span.form-text.gl-text-subtle + = _('Password for authentication with the observability service.') + + .form-group.col-md-6 + = f.label :o11y_service_post_message_encryption_key, _('Encryption Key'), class: 'label-bold' + = f.password_field :o11y_service_post_message_encryption_key, class: 'form-control', placeholder: (@settings.persisted? ? '••••••••' : nil), data: { testid: 'o11y-service-encryption-key-field' } + %span.form-text.gl-text-subtle + = _('32+ character encryption key for secure communication.') + + .form-group.gl-form-group + = f.submit _('Save settings'), pajamas_button: true, class: 'js-dirty-submit', data: { testid: 'save-o11y-service-settings-button' } + +- if @settings.persisted? + = render ::Layouts::SettingsBlockComponent.new(_('Danger Zone'), + id: 'js-o11y-service-settings-danger-zone', + testid: 'o11y-service-settings-danger-zone', + expanded: false) do |c| + - c.with_description do + = _('Permanently delete the observability service configuration.') + - c.with_body do + = link_button_to _('Delete settings'), group_observability_o11y_service_settings_path(@group), + method: :delete, + variant: :danger, + category: :secondary, + data: { confirm: _('Are you sure you want to delete the observability service settings? This action cannot be undone.'), + 'confirm-btn-variant': 'danger', + testid: 'delete-o11y-service-settings-button' } diff --git a/config/feature_flags/experiment/o11y_settings_access.yml b/config/feature_flags/experiment/o11y_settings_access.yml new file mode 100644 index 0000000000000000000000000000000000000000..a5ce624748b308a33b4b00de0df55130a04c690e --- /dev/null +++ b/config/feature_flags/experiment/o11y_settings_access.yml @@ -0,0 +1,8 @@ +--- +name: o11y_settings_access +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/197975 +rollout_issue_url: +milestone: '18.3' +type: experiment +group: group::embody +default_enabled: false diff --git a/config/routes/group.rb b/config/routes/group.rb index 0bbb069c68a9b77171d510279d19ec69f688df30..92f3f0f2aebf2e8b6bad9b38906a85f1219cdf77 100644 --- a/config/routes/group.rb +++ b/config/routes/group.rb @@ -185,6 +185,9 @@ resource :import_history, only: [:show] + namespace :observability do + resource :o11y_service_settings, only: [:update, :edit, :destroy] + end resources :observability, only: [:show] post :preview_markdown diff --git a/lib/sidebars/groups/menus/observability_menu.rb b/lib/sidebars/groups/menus/observability_menu.rb index 1b628a0afb3509b1e8674a91adc4db967d4c8920..3edd005007720904c4e4f5fd167262a90f4ed2cc 100644 --- a/lib/sidebars/groups/menus/observability_menu.rb +++ b/lib/sidebars/groups/menus/observability_menu.rb @@ -7,20 +7,24 @@ module Menus class ObservabilityMenu < ::Sidebars::Menu override :configure_menu_items def configure_menu_items - return false unless feature_enabled? - - add_item(services_menu_item) - add_item(traces_explorer_menu_item) - add_item(logs_explorer_menu_item) - add_item(metrics_explorer_menu_item) - add_item(infrastructure_monitoring_menu_item) - add_item(dashboard_menu_item) - add_item(messaging_queues_menu_item) - add_item(api_monitoring_menu_item) - add_item(alerts_menu_item) - add_item(exceptions_menu_item) - add_item(service_map_menu_item) - add_item(settings_menu_item) + return false unless feature_enabled? || o11y_settings_access_enabled? + + if context.group.observability_group_o11y_setting&.persisted? + add_item(services_menu_item) + add_item(traces_explorer_menu_item) + add_item(logs_explorer_menu_item) + add_item(metrics_explorer_menu_item) + add_item(infrastructure_monitoring_menu_item) + add_item(dashboard_menu_item) + add_item(messaging_queues_menu_item) + add_item(api_monitoring_menu_item) + add_item(alerts_menu_item) + add_item(exceptions_menu_item) + add_item(service_map_menu_item) + add_item(settings_menu_item) + end + + add_item(o11y_settings_menu_item) if o11y_settings_access_enabled? true end @@ -63,6 +67,10 @@ def feature_enabled? ::Feature.enabled?(:observability_sass_features, context.group) end + def o11y_settings_access_enabled? + ::Feature.enabled?(:o11y_settings_access, context.current_user) + end + def services_menu_item link = group_observability_path(context.group, 'services') ::Sidebars::MenuItem.new( @@ -206,6 +214,18 @@ def settings_menu_item container_html_options: { class: 'shortcuts-settings' } ) end + + def o11y_settings_menu_item + link = edit_group_observability_o11y_service_settings_path(context.group) + ::Sidebars::MenuItem.new( + title: _('O11y Service Settings'), + link: link, + active_routes: { page: link }, + super_sidebar_parent: ::Sidebars::Groups::SuperSidebarMenus::ObservabilityMenu, + item_id: :o11y_settings, + container_html_options: { class: 'shortcuts-o11y-settings' } + ) + end end end end diff --git a/lib/sidebars/groups/super_sidebar_menus/observability_menu.rb b/lib/sidebars/groups/super_sidebar_menus/observability_menu.rb index d4512e1972412cb8a353a15722d1add547e41639..5b7789a2749704811d19cb1829bf98bda0fab334 100644 --- a/lib/sidebars/groups/super_sidebar_menus/observability_menu.rb +++ b/lib/sidebars/groups/super_sidebar_menus/observability_menu.rb @@ -17,20 +17,7 @@ def sprite_icon override :configure_menu_items def configure_menu_items - [ - :services, - :traces_explorer, - :logs_explorer, - :metrics_explorer, - :infrastructure_monitoring, - :dashboard, - :messaging_queues, - :api_monitoring, - :alerts, - :exceptions, - :service_map, - :settings - ].each { |id| add_item(::Sidebars::NilMenuItem.new(item_id: id)) } + [:o11y_settings].each { |id| add_item(::Sidebars::NilMenuItem.new(item_id: id)) } end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index bac6157b237ba9c004a308ed50e0ff51f0c8dfe1..595aaf14d4c1a3f85f9e36c98838f6a556289b76 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -2079,6 +2079,9 @@ msgstr "" msgid "30+ contributions" msgstr "" +msgid "32+ character encryption key for secure communication." +msgstr "" + msgid "403|403: You do not have the permission to access this page" msgstr "" @@ -8712,6 +8715,9 @@ msgstr "" msgid "Are you sure you want to delete %{name}? This action cannot be undone." msgstr "" +msgid "Are you sure you want to delete the observability service settings? This action cannot be undone." +msgstr "" + msgid "Are you sure you want to delete this %{commentType}?" msgstr "" @@ -17419,6 +17425,9 @@ msgstr "" msgid "Configure your environments to be deployed to specific geographical regions" msgstr "" +msgid "Configure your observability service connection settings." +msgstr "" + msgid "Confirm" msgstr "" @@ -20268,6 +20277,9 @@ msgstr "" msgid "Daily" msgstr "" +msgid "Danger Zone" +msgstr "" + msgid "Dashboard" msgstr "" @@ -21552,6 +21564,9 @@ msgstr[1] "" msgid "Delete selected" msgstr "" +msgid "Delete settings" +msgstr "" + msgid "Delete snippet" msgstr "" @@ -24553,6 +24568,9 @@ msgstr "" msgid "Email address copied" msgstr "" +msgid "Email address for authentication with the observability service." +msgstr "" + msgid "Email address suffix" msgstr "" @@ -24928,6 +24946,9 @@ msgstr "" msgid "Enables a free Ultimate + GitLab Duo Enterprise trial when you create a new project." msgstr "" +msgid "Encryption Key" +msgstr "" + msgid "End Time" msgstr "" @@ -26704,6 +26725,9 @@ msgstr "" msgid "Failed to delete file! Please try again." msgstr "" +msgid "Failed to delete observability service settings." +msgstr "" + msgid "Failed to deploy to" msgstr "" @@ -42783,6 +42807,9 @@ msgstr "" msgid "Number of users for user cap" msgstr "" +msgid "O11y Service Settings" +msgstr "" + msgid "OAuth authorizations" msgstr "" @@ -42801,6 +42828,15 @@ msgstr "" msgid "Observability" msgstr "" +msgid "Observability Service Configuration" +msgstr "" + +msgid "Observability service settings deleted successfully." +msgstr "" + +msgid "Observability service settings updated successfully." +msgstr "" + msgid "ObservabilityLogs|Attribute" msgstr "" @@ -45159,6 +45195,9 @@ msgstr "" msgid "Password confirmation" msgstr "" +msgid "Password for authentication with the observability service." +msgstr "" + msgid "Password is required." msgstr "" @@ -45426,6 +45465,9 @@ msgstr "" msgid "Permalink copied to clipboard." msgstr "" +msgid "Permanently delete the observability service configuration." +msgstr "" + msgid "Permissions" msgstr "" @@ -54528,6 +54570,9 @@ msgstr "" msgid "Save password" msgstr "" +msgid "Save settings" +msgstr "" + msgid "Saving" msgstr "" @@ -58760,6 +58805,9 @@ msgstr "" msgid "Service Map" msgstr "" +msgid "Service URL" +msgstr "" + msgid "Service account" msgstr "" @@ -62835,6 +62883,9 @@ msgstr "" msgid "The Telegram bot token (for example, `123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11`)." msgstr "" +msgid "The URL of your observability service instance." +msgstr "" + msgid "The URL should start with http:// or https://" msgstr "" @@ -67588,6 +67639,9 @@ msgstr "" msgid "User API token. The user must have access to the task. All comments are attributed to this user." msgstr "" +msgid "User Email" +msgstr "" + msgid "User ID" msgstr "" diff --git a/spec/lib/sidebars/groups/menus/observability_menu_spec.rb b/spec/lib/sidebars/groups/menus/observability_menu_spec.rb index b02cf4965e785226350572a35cf809a12d6369f0..2ddd6aaec1c692678cdf9ed4307df220dcbd04a6 100644 --- a/spec/lib/sidebars/groups/menus/observability_menu_spec.rb +++ b/spec/lib/sidebars/groups/menus/observability_menu_spec.rb @@ -15,12 +15,71 @@ end describe '#configure_menu_items' do - context 'when feature flag is enabled' do + context 'when observability_sass_features feature flag is enabled' do before do stub_feature_flags(observability_sass_features: group) end - it 'adds all menu items' do + context 'when observability_group_o11y_setting is persisted' do + before do + stub_feature_flags(o11y_settings_access: false) + allow(group).to receive(:observability_group_o11y_setting).and_return(instance_double( + Observability::GroupO11ySetting, persisted?: true)) + end + + it 'adds all observability menu items' do + expected_menu_items = [ + :services, + :traces_explorer, + :logs_explorer, + :metrics_explorer, + :infrastructure_monitoring, + :dashboard, + :messaging_queues, + :api_monitoring, + :alerts, + :exceptions, + :service_map, + :settings + ] + + expect(observability_menu.renderable_items.map(&:item_id)).to match_array(expected_menu_items) + end + end + + context 'when observability_group_o11y_setting is not persisted' do + before do + stub_feature_flags(o11y_settings_access: false) + allow(group).to receive(:observability_group_o11y_setting).and_return(instance_double( + Observability::GroupO11ySetting, persisted?: false)) + end + + it 'does not add observability menu items' do + expect(observability_menu.renderable_items).to be_empty + end + end + end + + context 'when o11y_settings_access feature flag is enabled' do + before do + stub_feature_flags(observability_sass_features: false, o11y_settings_access: user) + end + + it 'adds the o11y settings menu item' do + expected_menu_items = [:o11y_settings] + + expect(observability_menu.renderable_items.map(&:item_id)).to match_array(expected_menu_items) + end + end + + context 'when both feature flags are enabled' do + before do + stub_feature_flags(observability_sass_features: group, o11y_settings_access: user) + allow(group).to receive(:observability_group_o11y_setting).and_return(instance_double( + Observability::GroupO11ySetting, persisted?: true)) + end + + it 'adds all menu items including o11y settings' do expected_menu_items = [ :services, :traces_explorer, @@ -33,19 +92,20 @@ :alerts, :exceptions, :service_map, - :settings + :settings, + :o11y_settings ] expect(observability_menu.renderable_items.map(&:item_id)).to match_array(expected_menu_items) end end - context 'when feature flag is disabled' do + context 'when both feature flags are disabled' do before do - stub_feature_flags(observability_sass_features: false) + stub_feature_flags(observability_sass_features: false, o11y_settings_access: false) end - it 'does not add any menu items' do + it 'returns false and does not add any menu items' do expect(observability_menu.configure_menu_items).to be false expect(observability_menu.renderable_items).to be_empty end @@ -54,7 +114,9 @@ describe '#title, #sprite_icon, #link' do before do - stub_feature_flags(observability_sass_features: group) + stub_feature_flags(observability_sass_features: group, o11y_settings_access: false) + allow(group).to receive(:observability_group_o11y_setting).and_return(instance_double( + Observability::GroupO11ySetting, persisted?: true)) observability_menu.configure_menu_items end @@ -91,7 +153,9 @@ describe '#menu items links' do before do - stub_feature_flags(observability_sass_features: group) + stub_feature_flags(observability_sass_features: group, o11y_settings_access: false) + allow(group).to receive(:observability_group_o11y_setting).and_return(instance_double( + Observability::GroupO11ySetting, persisted?: true)) observability_menu.configure_menu_items end @@ -115,6 +179,24 @@ expect(menu_items.find { |i| i.item_id == :alerts }.link).to include('alerts') expect(menu_items.find { |i| i.item_id == :exceptions }.link).to include('exceptions') expect(menu_items.find { |i| i.item_id == :service_map }.link).to include('service-map') + expect(menu_items.find { |i| i.item_id == :settings }.link).to include('settings') + end + end + + describe '#o11y_settings_menu_item' do + context 'when o11y_settings_access feature flag is enabled' do + before do + stub_feature_flags(observability_sass_features: false, o11y_settings_access: user) + observability_menu.configure_menu_items + end + + it 'has the right link for o11y settings menu item' do + menu_items = observability_menu.renderable_items + o11y_settings_item = menu_items.find { |i| i.item_id == :o11y_settings } + + expect(o11y_settings_item).not_to be_nil + expect(o11y_settings_item.link).to include('o11y_service_settings') + end end end end diff --git a/spec/requests/groups/observability/o11y_service_settings_spec.rb b/spec/requests/groups/observability/o11y_service_settings_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..c1ecd0fdb5633235a305763f3c5195b41f239a19 --- /dev/null +++ b/spec/requests/groups/observability/o11y_service_settings_spec.rb @@ -0,0 +1,194 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe "Groups::Observability::O11yServiceSettings", feature_category: :observability do + let_it_be(:group) { create(:group) } + let_it_be(:user) { create(:user) } + + before_all do + group.add_maintainer(user) + end + + before do + sign_in(user) + stub_feature_flags(o11y_settings_access: user) + end + + describe "GET /edit" do + subject(:edit_request) { get edit_group_observability_o11y_service_settings_path(group) } + + context 'with persisted settings' do + let_it_be(:settings) { create(:observability_group_o11y_setting, group: group) } + + it 'returns 200' do + edit_request + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template(:edit) + end + + it 'assigns @settings' do + edit_request + + expect(assigns(:settings)).to eq(settings) + end + end + + context 'without persisted settings' do + before do + group.observability_group_o11y_setting&.destroy! + group.reload + end + + it 'builds new settings' do + edit_request + + expect(assigns(:settings)).to be_a(Observability::GroupO11ySetting) + expect(assigns(:settings)).to be_new_record + expect(assigns(:settings).group).to eq(group) + end + end + + context 'when testing access control' do + context 'when feature flags are disabled' do + before do + stub_feature_flags(o11y_settings_access: false) + end + + it 'returns 404' do + edit_request + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + end + + describe "PUT /update" do + let_it_be(:settings) { create(:observability_group_o11y_setting, group: group) } + + let(:valid_params) do + { + observability_group_o11y_setting: { + o11y_service_url: 'https://new-o11y-instance.com', + o11y_service_user_email: 'newuser@example.com', + o11y_service_password: 'newpassword', + o11y_service_post_message_encryption_key: 'new-32-character-encryption-key-here' + } + } + end + + subject(:update_request) { put group_observability_o11y_service_settings_path(group), params: valid_params } + + context 'when updating existing settings' do + context 'with valid params' do + it 'calls the update service with correct parameters' do + expect_next_instance_of(Observability::GroupO11ySettingsUpdateService) do |service| + expect(service).to receive(:execute).with(an_instance_of(ActionController::Parameters)).and_return(true) + end + + update_request + end + + it 'updates the settings' do + expect { update_request }.to change { settings.reload.o11y_service_url }.to('https://new-o11y-instance.com') + end + + it 'redirects with success message' do + update_request + + expect(response).to redirect_to(edit_group_observability_o11y_service_settings_path(group)) + expect(flash[:notice]).to eq('Observability service settings updated successfully.') + end + end + + context 'with invalid params' do + let(:invalid_params) do + { + observability_group_o11y_setting: { + o11y_service_url: '', + o11y_service_user_email: 'invalid-email', + o11y_service_password: '', + o11y_service_post_message_encryption_key: '' + } + } + end + + it 'calls the update service with correct parameters' do + expect_next_instance_of(Observability::GroupO11ySettingsUpdateService) do |service| + expect(service).to receive(:execute).with(an_instance_of(ActionController::Parameters)).and_return(false) + end + + put group_observability_o11y_service_settings_path(group), params: invalid_params + end + + it 'renders edit template when service returns false' do + allow_next_instance_of(Observability::GroupO11ySettingsUpdateService) do |service| + allow(service).to receive(:execute).and_return(false) + end + + put group_observability_o11y_service_settings_path(group), params: invalid_params + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template(:edit) + end + + it 'does not update the settings when service returns false' do + allow_next_instance_of(Observability::GroupO11ySettingsUpdateService) do |service| + allow(service).to receive(:execute).and_return(false) + end + + expect { put group_observability_o11y_service_settings_path(group), params: invalid_params } + .not_to change { settings.reload.o11y_service_url } + end + end + + context 'when service execution fails' do + it 'renders edit template' do + allow_next_instance_of(Observability::GroupO11ySettingsUpdateService) do |service| + allow(service).to receive(:execute).and_return(false) + end + + update_request + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template(:edit) + end + end + end + end + + describe "DELETE /destroy" do + subject(:destroy_request) { delete group_observability_o11y_service_settings_path(group) } + + context 'with persisted settings' do + let!(:settings) { create(:observability_group_o11y_setting, group: group) } + + it 'deletes the settings' do + expect { destroy_request }.to change { Observability::GroupO11ySetting.count }.by(-1) + end + + it 'redirects with success message' do + destroy_request + + expect(response).to redirect_to(edit_group_observability_o11y_service_settings_path(group)) + expect(flash[:notice]).to eq('Observability service settings deleted successfully.') + end + end + + context 'without persisted settings' do + before do + group.observability_group_o11y_setting&.destroy! + group.reload + end + + it 'redirects with success message' do + destroy_request + + expect(response).to redirect_to(edit_group_observability_o11y_service_settings_path(group)) + expect(flash[:notice]).to eq('Observability service settings deleted successfully.') + end + end + end +end diff --git a/spec/services/observability/group_o11y_settings_update_service_spec.rb b/spec/services/observability/group_o11y_settings_update_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..e416422bcaf0dac4fb1bf39be08891936c1725b9 --- /dev/null +++ b/spec/services/observability/group_o11y_settings_update_service_spec.rb @@ -0,0 +1,133 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Observability::GroupO11ySettingsUpdateService, feature_category: :observability do + let_it_be(:group) { create(:group) } + let_it_be(:user) { create(:user) } + let_it_be(:settings) { create(:observability_group_o11y_setting, group: group) } + + let(:service) { described_class.new(settings) } + let(:settings_params) { {} } + + before_all do + group.add_owner(user) + end + + describe '#execute' do + subject(:execute) { service.execute(settings_params) } + + context 'when settings are persisted' do + it 'filters blank parameters before updating' do + params_with_blanks = { + o11y_service_url: 'https://new-example.com', + o11y_service_user_email: '', + o11y_service_password: nil, + o11y_service_post_message_encryption_key: '' + } + + expect(settings).to receive(:update) + .with({ o11y_service_url: 'https://new-example.com' }) + .and_return(true) + + result = service.execute(params_with_blanks) + expect(result).to be_truthy + end + + it 'updates settings with valid parameters' do + valid_params = { + o11y_service_url: 'https://new-example.com', + o11y_service_user_email: 'new@example.com', + o11y_service_password: 'password', + o11y_service_post_message_encryption_key: 'key' + } + + expect(settings).to receive(:update).with(valid_params).and_return(true) + + result = service.execute(valid_params) + expect(result).to be_truthy + end + + it 'handles parameters with only blank values by filtering to empty hash' do + blank_params = { + o11y_service_url: '', + o11y_service_user_email: nil, + o11y_service_password: ' ' + } + + expect(settings).to receive(:update).with({}).and_return(true) + + result = service.execute(blank_params) + expect(result).to be_truthy + end + + it 'handles empty parameters hash' do + expect(settings).to receive(:update).with({}).and_return(true) + + result = service.execute({}) + expect(result).to be_truthy + end + end + + context 'when settings are not persisted' do + let(:new_settings) { build(:observability_group_o11y_setting, group: group) } + let(:service) { described_class.new(new_settings) } + + it 'does not filter blank parameters' do + params_with_blanks = { + o11y_service_url: 'https://example.com', + o11y_service_user_email: '', + o11y_service_password: nil + } + params_without_blanks = { o11y_service_url: 'https://example.com' } + + expect(new_settings).to receive(:update).with(params_without_blanks).and_return(true) + + result = service.execute(params_with_blanks) + expect(result).to be_truthy + end + end + + context 'when update fails' do + it 'returns false when update fails' do + allow(settings).to receive(:update).and_return(false) + + result = service.execute(settings_params) + expect(result).to be_falsey + end + + it 'handles validation errors gracefully' do + allow(settings).to receive(:update).and_raise(ActiveRecord::RecordInvalid.new(settings)) + + expect { service.execute(settings_params) }.to raise_error(ActiveRecord::RecordInvalid) + end + end + end + + describe 'private methods' do + describe '#filter_blank_params' do + it 'removes blank values (empty strings, nil, whitespace)' do + params = { key1: 'value1', key2: '', key3: nil, key4: ' ', key5: 'value5' } + result = service.send(:filter_blank_params, params) + expect(result).to eq({ key1: 'value1', key5: 'value5' }) + end + + it 'keeps non-blank values unchanged' do + params = { key1: 'value1', key2: 'value2', key3: 'value3' } + result = service.send(:filter_blank_params, params) + expect(result).to eq(params) + end + + it 'handles empty hash' do + result = service.send(:filter_blank_params, {}) + expect(result).to eq({}) + end + + it 'handles non-string values correctly' do + params = { key1: 'value1', key2: false, key3: 'value3', key4: 'value' } + result = service.send(:filter_blank_params, params) + expect(result).to eq({ key1: 'value1', key3: 'value3', key4: 'value' }) + end + end + end +end diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb index c28fab606ffc8de36ba419aec7cc1a0a7fe23096..536ce1aa148c0dcc272bb7a59e67875b830f724d 100644 --- a/spec/support/shared_contexts/navbar_structure_context.rb +++ b/spec/support/shared_contexts/navbar_structure_context.rb @@ -191,20 +191,7 @@ let(:observability_nav_item) do { nav_item: _("Observability"), - nav_sub_items: [ - _("Services"), - _("Traces Explorer"), - _("Logs Explorer"), - _("Metrics Explorer"), - _("Infrastructure Monitoring"), - _("Dashboard"), - _("Messaging Queues"), - _("API Monitoring"), - _("Alerts"), - _("Exceptions"), - _("Service Map"), - _("Settings") - ] + nav_sub_items: [_("O11y Service Settings")] } end