diff --git a/ee/lib/remote_development/messages.rb b/ee/lib/remote_development/messages.rb index 14ab0d9d366bb2493b597296d2a8859e26c177d2..d998d0742a5b1da3e9a4b0566d24f0264f57c2c4 100644 --- a/ee/lib/remote_development/messages.rb +++ b/ee/lib/remote_development/messages.rb @@ -37,9 +37,10 @@ module Messages WorkspaceReconcileParamsValidationFailed = Class.new(Message) # Settings errors - SettingsEnvironmentVariableReadFailed = Class.new(Message) SettingsCurrentSettingsReadFailed = Class.new(Message) + SettingsEnvironmentVariableReadFailed = Class.new(Message) SettingsVscodeExtensionsGalleryValidationFailed = Class.new(Message) + SettingsVscodeExtensionsGalleryMetadataValidationFailed = Class.new(Message) # Namespace Cluster Agent Mapping create errors NamespaceClusterAgentMappingAlreadyExists = Class.new(Message) diff --git a/ee/lib/remote_development/settings/defaults_initializer.rb b/ee/lib/remote_development/settings/defaults_initializer.rb index 9d4397bc6cbeb43892389ae0a28df7f9f62e047a..c04f001d949c14104df35d91452f02d3688a2119 100644 --- a/ee/lib/remote_development/settings/defaults_initializer.rb +++ b/ee/lib/remote_development/settings/defaults_initializer.rb @@ -29,6 +29,10 @@ def self.default_settings resource_url_template: "https://open-vsx.org/api/{publisher}/{name}/{version}/file/{path}" }, Hash + ], + vscode_extensions_gallery_metadata: [ + {}, # NOTE: There is no default, the value is always generated by ExtensionsGalleryMetadataGenerator + Hash ] } end diff --git a/ee/lib/remote_development/settings/extensions_gallery_metadata_generator.rb b/ee/lib/remote_development/settings/extensions_gallery_metadata_generator.rb new file mode 100644 index 0000000000000000000000000000000000000000..4cb20daabbc6e1fe5e9f8f1e1bd9019232dae990 --- /dev/null +++ b/ee/lib/remote_development/settings/extensions_gallery_metadata_generator.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +module RemoteDevelopment + module Settings + class ExtensionsGalleryMetadataGenerator + include Messages + + # NOTE: These `disabled_reason` enumeration values are also referenced/consumed in + # the "gitlab-web-ide" and "gitlab-web-ide-vscode-fork" projects + # (https://gitlab.com/gitlab-org/gitlab-web-ide & https://gitlab.com/gitlab-org/gitlab-web-ide-vscode-fork), + # so we must ensure that any changes made here are also reflected in those projects. + DISABLED_REASONS = + %i[ + no_user + no_flag + instance_disabled + opt_in_unset + opt_in_disabled + ].to_h { |reason| [reason, reason] }.freeze + + # @param [Hash] value + # @return [Hash] + def self.generate(value) + value => { options: Hash => options } + options_with_defaults = { user: nil, vscode_extensions_marketplace_feature_flag_enabled: nil }.merge(options) + options_with_defaults => { + user: ::User | NilClass => user, + vscode_extensions_marketplace_feature_flag_enabled: TrueClass | FalseClass | NilClass => + extensions_marketplace_feature_flag_enabled + } + + extensions_gallery_metadata = generate_settings( + user: user, + flag_enabled: extensions_marketplace_feature_flag_enabled + ) + + value[:settings][:vscode_extensions_gallery_metadata] = extensions_gallery_metadata + value + end + + # @param [User, nil] user + # @param [Boolean, nil] flag_enabled + # @return [Hash] + def self.generate_settings(user:, flag_enabled:) + return { enabled: false, disabled_reason: DISABLED_REASONS.fetch(:no_user) } unless user + return { enabled: false, disabled_reason: DISABLED_REASONS.fetch(:no_flag) } if flag_enabled.nil? + return { enabled: false, disabled_reason: DISABLED_REASONS.fetch(:instance_disabled) } unless flag_enabled + + # noinspection RubyNilAnalysis -- RubyMine doesn't realize user can't be nil because of guard clause above + opt_in_status = user.extensions_marketplace_opt_in_status.to_sym + + return { enabled: true } if opt_in_status == :enabled + return { enabled: false, disabled_reason: DISABLED_REASONS.fetch(:opt_in_unset) } if opt_in_status == :unset + + if opt_in_status == :disabled + return { enabled: false, disabled_reason: DISABLED_REASONS.fetch(:opt_in_disabled) } + end + + # This is an internal bug due to an enumeration mismatch/inconsistency with the model, so lets throw an + # exception up the stack and let it be returned as a 500 - don't try to handle it via the ROP chain + raise "Invalid user.extensions_marketplace_opt_in_status: '#{opt_in_status}'. " \ + "Supported statuses are: #{Enums::WebIde::ExtensionsMarketplaceOptInStatus.statuses.keys}." # rubocop:disable Layout/LineEndStringConcatenationIndentation -- This is already changed in the next version of gitlab-styles + end + end + end +end diff --git a/ee/lib/remote_development/settings/extensions_gallery_metadata_validator.rb b/ee/lib/remote_development/settings/extensions_gallery_metadata_validator.rb new file mode 100644 index 0000000000000000000000000000000000000000..5434d5617b6a4880ca32819217ea53f52022e79f --- /dev/null +++ b/ee/lib/remote_development/settings/extensions_gallery_metadata_validator.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +module RemoteDevelopment + module Settings + class ExtensionsGalleryMetadataValidator + include Messages + + # @param [Hash] value + # @return [Result] + def self.validate(value) + value => { settings: Hash => settings } + settings => { vscode_extensions_gallery_metadata: Hash => extensions_gallery_metadata } + + validatable_hash = make_hash_validatable_by_json_schemer(extensions_gallery_metadata) + errors = validate_against_schema(validatable_hash) + + if errors.none? + Result.ok(value) + else + Result.err(SettingsVscodeExtensionsGalleryMetadataValidationFailed.new(details: errors.join(". "))) + end + end + + # @param [Hash] hash + # @return [Hash] + def self.make_hash_validatable_by_json_schemer(hash) + hash + .deep_stringify_keys + .transform_values { |v| v.is_a?(Symbol) ? v.to_s : v } + end + + # @param [Hash] hash_to_validate + # @return [Array] + def self.validate_against_schema(hash_to_validate) + schema = { + "properties" => { + "enabled" => { + "type" => "boolean" + } + }, + # do conditional check that "enabled" is boolean type + "if" => { + "properties" => { + "enabled" => { + "type" => "boolean" + } + } + }, + "then" => { + # "enabled" is boolean, do conditional check for "enabled" value + "if" => { + "properties" => { + "enabled" => { + "const" => true + } + } + }, + "then" => { + # "enabled" is true, "disabled_reason" is not required + "required" => %w[enabled] + }, + "else" => { + # "enabled" is false, "disabled_reason" is required + "required" => %w[enabled disabled_reason], + "properties" => { + "disabled_reason" => { + "type" => "string" + } + } + } + } + } + + schemer = JSONSchemer.schema(schema) + errors = schemer.validate(hash_to_validate) + errors.map { |error| JSONSchemer::Errors.pretty(error) } + end + end + end +end diff --git a/ee/lib/remote_development/settings/extensions_gallery_validator.rb b/ee/lib/remote_development/settings/extensions_gallery_validator.rb index aa44de52ec61ef239fb8b8e17cb9aae9ff4b0106..a3c158de793d6d7e20d27126ec44b9b5509b2d79 100644 --- a/ee/lib/remote_development/settings/extensions_gallery_validator.rb +++ b/ee/lib/remote_development/settings/extensions_gallery_validator.rb @@ -23,9 +23,9 @@ def self.validate(value) end end - # @param [Hash] json_to_validate + # @param [Hash] hash_to_validate # @return [Array] - def self.validate_against_schema(json_to_validate) + def self.validate_against_schema(hash_to_validate) schema = { "required" => %w[ @@ -47,7 +47,7 @@ def self.validate_against_schema(json_to_validate) } schemer = JSONSchemer.schema(schema) - errors = schemer.validate(json_to_validate) + errors = schemer.validate(hash_to_validate) errors.map { |error| JSONSchemer::Errors.pretty(error) } end end diff --git a/ee/lib/remote_development/settings/main.rb b/ee/lib/remote_development/settings/main.rb index d0aa0734a5c65f5de6ed8ca733119e5c586646e0..b926f45196cb0cc6031d8746575e3fb7bb92c443 100644 --- a/ee/lib/remote_development/settings/main.rb +++ b/ee/lib/remote_development/settings/main.rb @@ -8,10 +8,11 @@ class Main extend MessageSupport private_class_method :generate_error_response_from_message + # @param [Hash] value # @return [Hash] # @raise [UnmatchedResultError] - def self.get_settings - initial_result = Result.ok({}) + def self.get_settings(value) + initial_result = Result.ok(value) # The order of the chain determines the precedence of settings. I.e., defaults are # overridden by env vars, and any subsequent steps override env vars. @@ -19,10 +20,12 @@ def self.get_settings initial_result .map(DefaultsInitializer.method(:init)) .and_then(CurrentSettingsReader.method(:read)) + .map(ExtensionsGalleryMetadataGenerator.method(:generate)) # NOTE: EnvVarReader is kept as last step, so it can always be used to easily override any settings for # local or temporary testing. .and_then(EnvVarReader.method(:read)) .and_then(RemoteDevelopment::Settings::ExtensionsGalleryValidator.method(:validate)) + .and_then(RemoteDevelopment::Settings::ExtensionsGalleryMetadataValidator.method(:validate)) .map( # As the final step, return the settings in a SettingsGetSuccessful message ->(value) do @@ -39,6 +42,8 @@ def self.get_settings generate_error_response_from_message(message: message, reason: :internal_server_error) in { err: SettingsVscodeExtensionsGalleryValidationFailed => message } generate_error_response_from_message(message: message, reason: :internal_server_error) + in { err: SettingsVscodeExtensionsGalleryMetadataValidationFailed => message } + generate_error_response_from_message(message: message, reason: :internal_server_error) in { ok: SettingsGetSuccessful => message } { settings: message.context.fetch(:settings), status: :success } else diff --git a/ee/lib/remote_development/settings/public_api.rb b/ee/lib/remote_development/settings/public_api.rb index 82febf6f576b282d8a443bb9d12bcff00faf7024..3152d5f68832acdaaaae473c591688f2c96a630a 100644 --- a/ee/lib/remote_development/settings/public_api.rb +++ b/ee/lib/remote_development/settings/public_api.rb @@ -11,21 +11,23 @@ module Settings # RemoteDevelopment::Settings::Main.get_settings module PublicApi # @param [Symbol] setting_name + # @param [Hash] options # @return [Object] # @raise [RuntimeError] - def get_single_setting(setting_name) + def get_single_setting(setting_name, options = {}) raise "Setting name must be a Symbol" unless setting_name.is_a?(Symbol) is_valid_setting_name = get_all_settings.key?(setting_name) raise "Unsupported Remote Development setting name: '#{setting_name}'" unless is_valid_setting_name - get_all_settings.fetch(setting_name) + get_all_settings(options).fetch(setting_name) end + # @param [Hash] options # @return [Hash] # @raise [RuntimeError] - def get_all_settings - response_hash = RemoteDevelopment::Settings::Main.get_settings + def get_all_settings(options = {}) + response_hash = RemoteDevelopment::Settings::Main.get_settings({ options: options }) raise response_hash.fetch(:message).to_s if response_hash.fetch(:status) == :error diff --git a/ee/spec/lib/remote_development/settings/extensions_gallery_metadata_generator_spec.rb b/ee/spec/lib/remote_development/settings/extensions_gallery_metadata_generator_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..1969fbbe5a9912c5cb5a0b360b904c8b9669ecb1 --- /dev/null +++ b/ee/spec/lib/remote_development/settings/extensions_gallery_metadata_generator_spec.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe RemoteDevelopment::Settings::ExtensionsGalleryMetadataGenerator, :rd_fast, feature_category: :remote_development do + using RSpec::Parameterized::TableSyntax + + let(:input_value) do + { + options: options, + settings: { + # NOTE: default value of 'vscode_extensions_gallery_metadata' is an empty hash. Include it here to + # ensure that it always gets overwritten with the generated value + vscode_extensions_gallery_metadata: {}, + some_other_existing_setting_that_should_not_be_overwritten: "some value" + } + } + end + + subject(:returned_value) do + described_class.generate(input_value) + end + + shared_examples 'extensions marketplace settings' do + it "has the expected settings behavior" do + if expected_vscode_extensions_gallery_metadata == RuntimeError + expected_err_msg = "Invalid user.extensions_marketplace_opt_in_status: '#{opt_in_status}'. " \ + "Supported statuses are: [:unset, :enabled, :disabled]." # rubocop:disable Layout/LineEndStringConcatenationIndentation -- This is already changed in the next version of gitlab-styles + expect { returned_value } + .to raise_error(expected_err_msg) + else + expect(returned_value).to eq( + input_value.deep_merge( + settings: { + vscode_extensions_gallery_metadata: expected_vscode_extensions_gallery_metadata + } + ) + ) + end + end + end + + where( + :user_exists, + :opt_in_status, + :flag_exists, + :flag_enabled, + :expected_vscode_extensions_gallery_metadata + ) do + # @formatter:off - Turn off RubyMine autoformatting + + # user exists | opt_in_status | flag exists | flag_enabled | expected_settings + false | :undefined | false | :undefined | { enabled: false, disabled_reason: :no_user } + false | :undefined | true | true | { enabled: false, disabled_reason: :no_user } + true | :unset | false | :undefined | { enabled: false, disabled_reason: :no_flag } + true | :unset | true | false | { enabled: false, disabled_reason: :instance_disabled } + true | :unset | true | true | { enabled: false, disabled_reason: :opt_in_unset } + true | :disabled | true | true | { enabled: false, disabled_reason: :opt_in_disabled } + true | :enabled | true | true | { enabled: true } + true | :invalid | true | true | RuntimeError + + # @formatter:on + end + + with_them do + let(:user) { create(:user) } + + let(:options) do + options = {} + options[:user] = user if user_exists + options[:vscode_extensions_marketplace_feature_flag_enabled] = flag_enabled if flag_exists + options + end + + before do + allow(user).to receive(:extensions_marketplace_opt_in_status) { opt_in_status.to_s } + end + + it_behaves_like "extensions marketplace settings" + end +end diff --git a/ee/spec/lib/remote_development/settings/extensions_gallery_metadata_validator_spec.rb b/ee/spec/lib/remote_development/settings/extensions_gallery_metadata_validator_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..e3fb3bdb5c6cb040cce3a063f4f2019fef2ee25f --- /dev/null +++ b/ee/spec/lib/remote_development/settings/extensions_gallery_metadata_validator_spec.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +require_relative "../rd_fast_spec_helper" + +RSpec.describe RemoteDevelopment::Settings::ExtensionsGalleryMetadataValidator, :rd_fast, feature_category: :remote_development do + include ResultMatchers + + let(:value) do + { + settings: { + vscode_extensions_gallery_metadata: extensions_gallery_metadata + } + } + end + + subject(:result) do + described_class.validate(value) + end + + context "when vscode_extensions_gallery_metadata is valid" do + shared_examples "success result" do + it "return an ok Result containing the original value which was passed" do + expect(result).to eq(Result.ok(value)) + end + end + + context "when enabled is true" do + let(:extensions_gallery_metadata) do + { + enabled: true + } + end + + it_behaves_like 'success result' + end + + context "when enabled is false and disabled_reason is present" do + let(:extensions_gallery_metadata) do + { + enabled: false, + disabled_reason: :no_user + } + end + + it_behaves_like 'success result' + end + end + + context "when vscode_extensions_gallery_metadata is invalid" do + shared_examples "err result" do |expected_error_details:| + it "returns an err Result containing error details" do + expect(result).to be_err_result do |message| + expect(message) + .to be_a(RemoteDevelopment::Messages::SettingsVscodeExtensionsGalleryMetadataValidationFailed) + message.context => { details: String => error_details } + expect(error_details).to eq(expected_error_details) + end + end + end + + context "when empty" do + let(:extensions_gallery_metadata) { {} } + + it_behaves_like "err result", expected_error_details: "root is missing required keys: enabled" + end + + context "when enabled is missing but disabled_reason is present" do + let(:extensions_gallery_metadata) { { disabled_reason: :no_user } } + + it_behaves_like "err result", expected_error_details: "root is missing required keys: enabled" + end + + context "when enabled is false but disabled_reason is missing" do + let(:extensions_gallery_metadata) do + { + enabled: false + } + end + + it_behaves_like "err result", expected_error_details: "root is missing required keys: disabled_reason" + end + + context "for enabled" do + context "when not a boolean" do + let(:extensions_gallery_metadata) do + { + enabled: "not a boolean" + } + end + + it_behaves_like "err result", expected_error_details: "property '/enabled' is not of type: boolean" + end + end + + context "for disabled_reason" do + context "when not a string" do + let(:extensions_gallery_metadata) do + { + enabled: false, + disabled_reason: 1 + } + end + + it_behaves_like "err result", expected_error_details: + "property '/disabled_reason' is not of type: string" + end + end + end +end diff --git a/ee/spec/lib/remote_development/settings/extensions_gallery_validator_spec.rb b/ee/spec/lib/remote_development/settings/extensions_gallery_validator_spec.rb index 25eea0572e2b48e68b03c5daba3e5dbfa53c53fa..22156d6c05e24c555c8dc4cc8026b6181e0d4bbf 100644 --- a/ee/spec/lib/remote_development/settings/extensions_gallery_validator_spec.rb +++ b/ee/spec/lib/remote_development/settings/extensions_gallery_validator_spec.rb @@ -55,16 +55,14 @@ let(:vscode_extensions_gallery) { {} } it_behaves_like "err result", expected_error_details: - "root is missing required keys: service_url, item_url, " \ - "resource_url_template" # rubocop:disable Layout/LineEndStringConcatenationIndentation -- This is being changed in https://gitlab.com/gitlab-org/ruby/gems/gitlab-styles/-/merge_requests/212 + "root is missing required keys: service_url, item_url, resource_url_template" end context "for service_url" do context "when not a string" do let(:service_url) { { not_a_string: true } } - it_behaves_like "err result", expected_error_details: - "property '/service_url' is not of type: string" + it_behaves_like "err result", expected_error_details: "property '/service_url' is not of type: string" end end @@ -72,8 +70,7 @@ context "when not a string" do let(:item_url) { { not_a_string: true } } - it_behaves_like "err result", expected_error_details: - "property '/item_url' is not of type: string" + it_behaves_like "err result", expected_error_details: "property '/item_url' is not of type: string" end end @@ -81,8 +78,7 @@ context "when not a string" do let(:resource_url_template) { { not_a_string: true } } - it_behaves_like "err result", expected_error_details: - "property '/resource_url_template' is not of type: string" + it_behaves_like "err result", expected_error_details: "property '/resource_url_template' is not of type: string" end end end diff --git a/ee/spec/lib/remote_development/settings/main_spec.rb b/ee/spec/lib/remote_development/settings/main_spec.rb index b38c56466724c98156c8acd6d62b0e64fe0c0e43..7b47c58573a0e10fc2c0591639c0c50a82e7ea78 100644 --- a/ee/spec/lib/remote_development/settings/main_spec.rb +++ b/ee/spec/lib/remote_development/settings/main_spec.rb @@ -5,46 +5,56 @@ RSpec.describe RemoteDevelopment::Settings::Main, :rd_fast, feature_category: :remote_development do include RemoteDevelopment::RailwayOrientedProgrammingHelpers + let(:input_value) { { some_context: true } } let(:settings) { { some_setting: 42 } } - let(:value) { { settings: settings } } + let(:value) { { some_context: true, settings: settings } } let(:error_details) { 'some error details' } let(:err_message_context) { { details: error_details } } + # rubocop:disable Layout/LineLength -- keep all the class and method fixtures as single-liners easier scanning/editing # Classes let(:defaults_initializer_class) { RemoteDevelopment::Settings::DefaultsInitializer } let(:current_settings_reader_class) { RemoteDevelopment::Settings::CurrentSettingsReader } + let(:extensions_gallery_metadata_generator_class) { RemoteDevelopment::Settings::ExtensionsGalleryMetadataGenerator } let(:env_var_reader_class) { RemoteDevelopment::Settings::EnvVarReader } let(:extensions_gallery_validator_class) { RemoteDevelopment::Settings::ExtensionsGalleryValidator } + let(:extensions_gallery_metadata_validator_class) { RemoteDevelopment::Settings::ExtensionsGalleryMetadataValidator } # Methods let(:defaults_initializer_method) { defaults_initializer_class.singleton_method(:init) } let(:current_settings_reader_method) { current_settings_reader_class.singleton_method(:read) } + let(:extensions_gallery_metadata_generator_method) { extensions_gallery_metadata_generator_class.singleton_method(:generate) } let(:env_var_reader_method) { env_var_reader_class.singleton_method(:read) } - let(:extensions_gallery_validator_method) do - extensions_gallery_validator_class.singleton_method(:validate) - end + let(:extensions_gallery_validator_method) { extensions_gallery_validator_class.singleton_method(:validate) } + let(:extensions_gallery_metadata_validator_method) { extensions_gallery_metadata_validator_class.singleton_method(:validate) } # Subject - subject(:response) { described_class.get_settings } + subject(:response) { described_class.get_settings(input_value) } before do allow(defaults_initializer_class).to receive(:method).with(:init) { defaults_initializer_method } allow(current_settings_reader_class).to receive(:method).with(:read) { current_settings_reader_method } + allow(extensions_gallery_metadata_generator_class).to(receive(:method).with(:generate)) { extensions_gallery_metadata_generator_method } allow(env_var_reader_class).to receive(:method).with(:read) { env_var_reader_method } - allow(extensions_gallery_validator_class).to receive(:method).with(:validate) do - extensions_gallery_validator_method - end + allow(extensions_gallery_validator_class).to(receive(:method).with(:validate)) { extensions_gallery_validator_method } + allow(extensions_gallery_metadata_validator_class).to(receive(:method).with(:validate)) { extensions_gallery_metadata_validator_method } - stub_method_to_modify_and_return_value(defaults_initializer_method, expected_value: {}, returned_value: value) + stub_method_to_modify_and_return_value(defaults_initializer_method, expected_value: input_value, returned_value: value) + stub_methods_to_return_value(extensions_gallery_metadata_generator_method) end + # rubocop:enable Layout/LineLength context 'when all steps are successful' do before do - stub_methods_to_return_ok_result(current_settings_reader_method, env_var_reader_method, - extensions_gallery_validator_method) + stub_methods_to_return_ok_result( + current_settings_reader_method, + env_var_reader_method, + extensions_gallery_validator_method, + extensions_gallery_metadata_validator_method + ) end it 'returns a success response with the settings as the payload' do @@ -108,6 +118,28 @@ end end + context 'when the ExtensionsGalleryMetadataValidator returns an err Result' do + before do + stub_methods_to_return_ok_result( + current_settings_reader_method, + env_var_reader_method, + extensions_gallery_validator_method + ) + stub_methods_to_return_err_result( + method: extensions_gallery_metadata_validator_method, + message_class: RemoteDevelopment::Messages::SettingsVscodeExtensionsGalleryMetadataValidationFailed + ) + end + + it 'returns an error response' do + expect(response).to eq({ + status: :error, + message: "Settings VSCode extensions gallery metadata validation failed: #{error_details}", + reason: :internal_server_error + }) + end + end + context 'when an invalid Result is returned' do before do stub_methods_to_return_ok_result(current_settings_reader_method) diff --git a/ee/spec/lib/remote_development/settings/public_api_spec.rb b/ee/spec/lib/remote_development/settings/public_api_spec.rb index da0bd5536c60c1ff538e13f9965a4f835e53fe29..344bb24e2988ba6c183ec132cd89439fa3121952 100644 --- a/ee/spec/lib/remote_development/settings/public_api_spec.rb +++ b/ee/spec/lib/remote_development/settings/public_api_spec.rb @@ -1,27 +1,52 @@ # frozen_string_literal: true -require 'spec_helper' +require_relative '../rd_fast_spec_helper' -RSpec.describe RemoteDevelopment::Settings::PublicApi, feature_category: :remote_development do - describe "get_single_setting" do - context "when passed a valid setting name" do - it "returns the setting value" do - expect(RemoteDevelopment::Settings.get_single_setting(:max_hours_before_termination_limit)).to eq(120) +RSpec.describe RemoteDevelopment::Settings::PublicApi, :rd_fast, feature_category: :remote_development do + subject(:settings_module) { RemoteDevelopment::Settings } + + before do + allow(RemoteDevelopment::Settings::Main).to receive(:get_settings).with({ options: {} }).and_return(response_hash) + end + + context "when successful" do + let(:response_hash) { { settings: { some_setting: 42 }, status: :success } } + + describe "get_single_setting" do + context "when passed a valid setting name" do + it "returns the setting value" do + expect(settings_module.get_single_setting(:some_setting)).to eq(42) + end + end + + context "when passed options" do + let(:options) { { some_options_key: true } } + + it "passes along the options and returns the setting value" do + expect(settings_module::Main).to receive(:get_settings).with({ options: options }).and_return(response_hash) + expect(settings_module.get_single_setting(:some_setting, options)).to eq(42) + end end end - context "when passed an invalid setting name" do - it "raises an exception with a descriptive message" do - expect { RemoteDevelopment::Settings.get_single_setting(:invalid_setting_name) } - .to raise_error("Unsupported Remote Development setting name: 'invalid_setting_name'") + describe "get_all_settings" do + it "returns a Hash containing all settings" do + expect(settings_module.get_all_settings) + .to match(hash_including(some_setting: 42)) end end end - describe "get_all_settings" do - it "returns a Hash containing all settings" do - expect(RemoteDevelopment::Settings.get_all_settings) - .to match(hash_including(max_hours_before_termination_limit: 120)) + context "when unsuccessful" do + let(:response_hash) { { status: :error, message: :failed } } + + describe "get_single_setting" do + context "when passed an invalid setting name" do + it "raises an exception with a descriptive message" do + expect { settings_module.get_single_setting(:invalid_setting_name) } + .to raise_error("failed") + end + end end end end diff --git a/ee/spec/lib/remote_development/settings/settings_integration_spec.rb b/ee/spec/lib/remote_development/settings/settings_integration_spec.rb index 380d8545a460aa34e1527eb93e0dac6d9143bbd7..08b0c182896b7fe8ec645897f676a7b35a0cec00 100644 --- a/ee/spec/lib/remote_development/settings/settings_integration_spec.rb +++ b/ee/spec/lib/remote_development/settings/settings_integration_spec.rb @@ -3,9 +3,7 @@ require 'spec_helper' RSpec.describe ::RemoteDevelopment::Settings, feature_category: :remote_development do # rubocop:disable RSpec/FilePath -- Not sure why this is being flagged - subject(:settings_module) do - described_class - end + subject(:settings_module) { described_class } context "when there is no override" do before do @@ -47,4 +45,74 @@ expect(settings_module.get_single_setting(:default_branch_name)).to eq(override_value_from_env) end end + + context "when passed an invalid setting name" do + it "uses default value" do + expect { settings_module.get_single_setting(:invalid_setting_name) } + .to raise_error("Unsupported Remote Development setting name: 'invalid_setting_name'") + end + end + + context "for vscode_extensions_gallery setting" do + subject(:vscode_extensions_gallery_setting) { settings_module.get_single_setting(:vscode_extensions_gallery) } + + it "uses default value" do + expected_value = { + item_url: "https://open-vsx.org/vscode/item", + resource_url_template: "https://open-vsx.org/api/{publisher}/{name}/{version}/file/{path}", + service_url: "https://open-vsx.org/vscode/gallery" + } + + expect(vscode_extensions_gallery_setting).to eq(expected_value) + end + + context "when invalid value is set" do + before do + stub_env("GITLAB_REMOTE_DEVELOPMENT_VSCODE_EXTENSIONS_GALLERY", '{"foo":"bar"}') + end + + it "raises an error" do + expected_err_msg = "Settings VSCode extensions gallery validation failed: root is missing required keys: " \ + "service_url, item_url, resource_url_template" # rubocop:disable Layout/LineEndStringConcatenationIndentation -- This is being changed in https://gitlab.com/gitlab-org/ruby/gems/gitlab-styles/-/merge_requests/212 + expect { vscode_extensions_gallery_setting } + .to raise_error(expected_err_msg) + end + end + end + + context "for vscode_extensions_gallery_metadata setting" do + let_it_be(:user) { create(:user) } + let_it_be(:options) do + { + user: user, + vscode_extensions_marketplace_feature_flag_enabled: false + } + end + + subject(:vscode_extensions_gallery_metadata_setting) do + settings_module.get_single_setting(:vscode_extensions_gallery_metadata, options) + end + + it "uses default value" do + expected_value = { + enabled: false, + disabled_reason: :instance_disabled + } + + expect(vscode_extensions_gallery_metadata_setting).to eq(expected_value) + end + + context "when invalid value is set" do + before do + stub_env("GITLAB_REMOTE_DEVELOPMENT_VSCODE_EXTENSIONS_GALLERY_METADATA", '{"foo":"bar"}') + end + + it "raises an error" do + expected_err_msg = "Settings VSCode extensions gallery metadata validation failed: " \ + "root is missing required keys: enabled" # rubocop:disable Layout/LineEndStringConcatenationIndentation -- This is being changed in https://gitlab.com/gitlab-org/ruby/gems/gitlab-styles/-/merge_requests/212 + expect { vscode_extensions_gallery_metadata_setting } + .to raise_error(expected_err_msg) + end + end + end end