diff --git a/lib/optimizely.rb b/lib/optimizely.rb index 86d405f8..63753a32 100644 --- a/lib/optimizely.rb +++ b/lib/optimizely.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # -# Copyright 2016-2022, Optimizely and contributors +# Copyright 2016-2023, Optimizely and contributors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -36,6 +36,7 @@ require_relative 'optimizely/helpers/variable_type' require_relative 'optimizely/logger' require_relative 'optimizely/notification_center' +require_relative 'optimizely/notification_center_registry' require_relative 'optimizely/optimizely_config' require_relative 'optimizely/optimizely_user_context' require_relative 'optimizely/odp/lru_cache' @@ -105,19 +106,7 @@ def initialize( # rubocop:disable Metrics/ParameterLists @notification_center = notification_center.is_a?(Optimizely::NotificationCenter) ? notification_center : NotificationCenter.new(@logger, @error_handler) - setup_odp! - - @odp_manager = OdpManager.new( - disable: @sdk_settings.odp_disabled, - segment_manager: @sdk_settings.odp_segment_manager, - event_manager: @sdk_settings.odp_event_manager, - segments_cache: @sdk_settings.odp_segments_cache, - fetch_segments_timeout: @sdk_settings.fetch_segments_timeout, - odp_event_timeout: @sdk_settings.odp_event_timeout, - logger: @logger - ) - - @config_manager = if config_manager.respond_to?(:config) + @config_manager = if config_manager.respond_to?(:config) && config_manager.respond_to?(:sdk_key) config_manager elsif sdk_key HTTPProjectConfigManager.new( @@ -132,9 +121,7 @@ def initialize( # rubocop:disable Metrics/ParameterLists StaticProjectConfigManager.new(datafile, @logger, @error_handler, skip_json_validation) end - # must call this even if it's scheduled as a listener - # in case the config manager was initialized before the listener was added - update_odp_config_on_datafile_update unless @sdk_settings.odp_disabled + setup_odp!(@config_manager.sdk_key) @decision_service = DecisionService.new(@logger, @user_profile_service) @@ -1171,7 +1158,7 @@ def project_config end def update_odp_config_on_datafile_update - # if datafile isn't ready, expects to be called again by the notification_center + # if datafile isn't ready, expects to be called again by the internal notification_center return if @config_manager.respond_to?(:ready?) && !@config_manager.ready? config = @config_manager&.config @@ -1180,19 +1167,12 @@ def update_odp_config_on_datafile_update @odp_manager.update_odp_config(config.public_key_for_odp, config.host_for_odp, config.all_segments) end - def setup_odp! + def setup_odp!(sdk_key) unless @sdk_settings.is_a? Optimizely::Helpers::OptimizelySdkSettings @logger.log(Logger::DEBUG, 'Provided sdk_settings is not an OptimizelySdkSettings instance.') unless @sdk_settings.nil? @sdk_settings = Optimizely::Helpers::OptimizelySdkSettings.new end - return if @sdk_settings.odp_disabled - - @notification_center.add_notification_listener( - NotificationCenter::NOTIFICATION_TYPES[:OPTIMIZELY_CONFIG_UPDATE], - -> { update_odp_config_on_datafile_update } - ) - if !@sdk_settings.odp_segment_manager.nil? && !Helpers::Validator.segment_manager_valid?(@sdk_settings.odp_segment_manager) @logger.log(Logger::ERROR, 'Invalid ODP segment manager, reverting to default.') @sdk_settings.odp_segment_manager = nil @@ -1203,17 +1183,39 @@ def setup_odp! @sdk_settings.odp_event_manager = nil end - return if @sdk_settings.odp_segment_manager - if !@sdk_settings.odp_segments_cache.nil? && !Helpers::Validator.segments_cache_valid?(@sdk_settings.odp_segments_cache) @logger.log(Logger::ERROR, 'Invalid ODP segments cache, reverting to default.') @sdk_settings.odp_segments_cache = nil end - @sdk_settings.odp_segments_cache ||= LRUCache.new( - @sdk_settings.segments_cache_size, - @sdk_settings.segments_cache_timeout_in_secs + # no need to instantiate a cache if a custom cache or segment manager is provided. + if !@sdk_settings.odp_disabled && @sdk_settings.odp_segment_manager.nil? + @sdk_settings.odp_segments_cache ||= LRUCache.new( + @sdk_settings.segments_cache_size, + @sdk_settings.segments_cache_timeout_in_secs + ) + end + + @odp_manager = OdpManager.new( + disable: @sdk_settings.odp_disabled, + segment_manager: @sdk_settings.odp_segment_manager, + event_manager: @sdk_settings.odp_event_manager, + segments_cache: @sdk_settings.odp_segments_cache, + fetch_segments_timeout: @sdk_settings.fetch_segments_timeout, + odp_event_timeout: @sdk_settings.odp_event_timeout, + logger: @logger ) + + return if @sdk_settings.odp_disabled + + Optimizely::NotificationCenterRegistry + .get_notification_center(sdk_key, @logger) + &.add_notification_listener( + NotificationCenter::NOTIFICATION_TYPES[:OPTIMIZELY_CONFIG_UPDATE], + method(:update_odp_config_on_datafile_update) + ) + + update_odp_config_on_datafile_update end end end diff --git a/lib/optimizely/config_manager/http_project_config_manager.rb b/lib/optimizely/config_manager/http_project_config_manager.rb index 790353ab..0da73c1f 100644 --- a/lib/optimizely/config_manager/http_project_config_manager.rb +++ b/lib/optimizely/config_manager/http_project_config_manager.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # -# Copyright 2019-2020, 2022, Optimizely and contributors +# Copyright 2019-2020, 2022-2023, Optimizely and contributors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -33,12 +33,12 @@ module Optimizely class HTTPProjectConfigManager < ProjectConfigManager # Config manager that polls for the datafile and updated ProjectConfig based on an update interval. - attr_reader :stopped + attr_reader :stopped, :sdk_key # Initialize config manager. One of sdk_key or url has to be set to be able to use. # - # sdk_key - Optional string uniquely identifying the datafile. It's required unless a URL is passed in. - # datafile: Optional JSON string representing the project. + # sdk_key - Optional string uniquely identifying the datafile. It's required unless a datafile with sdk_key is passed in. + # datafile - Optional JSON string representing the project. If nil, sdk_key is required. # polling_interval - Optional floating point number representing time interval in seconds # at which to request datafile and set ProjectConfig. # blocking_timeout - Optional Time in seconds to block the config call until config object has been initialized. @@ -83,6 +83,10 @@ def initialize( @notification_center = notification_center.is_a?(Optimizely::NotificationCenter) ? notification_center : NotificationCenter.new(@logger, @error_handler) @optimizely_config = nil @config = datafile.nil? ? nil : DatafileProjectConfig.create(datafile, @logger, @error_handler, @skip_json_validation) + @sdk_key = sdk_key || @config&.sdk_key + + raise MissingSdkKeyError if @sdk_key.nil? + @mutex = Mutex.new @resource = ConditionVariable.new @async_scheduler = AsyncScheduler.new(method(:fetch_datafile_config), @polling_interval, auto_update, @logger) @@ -222,6 +226,10 @@ def set_config(config) @notification_center.send_notifications(NotificationCenter::NOTIFICATION_TYPES[:OPTIMIZELY_CONFIG_UPDATE]) + NotificationCenterRegistry + .get_notification_center(@sdk_key, @logger) + &.send_notifications(NotificationCenter::NOTIFICATION_TYPES[:OPTIMIZELY_CONFIG_UPDATE]) + @logger.log(Logger::DEBUG, 'Received new datafile and updated config. ' \ "Old revision number: #{previous_revision}. New revision number: #{@config.revision}.") end diff --git a/lib/optimizely/config_manager/project_config_manager.rb b/lib/optimizely/config_manager/project_config_manager.rb index e0a3f8e8..220df9ae 100644 --- a/lib/optimizely/config_manager/project_config_manager.rb +++ b/lib/optimizely/config_manager/project_config_manager.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # -# Copyright 2019, Optimizely and contributors +# Copyright 2019, 2023, Optimizely and contributors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -20,5 +20,6 @@ class ProjectConfigManager # Interface for fetching ProjectConfig instance. def config; end + def sdk_key; end end end diff --git a/lib/optimizely/config_manager/static_project_config_manager.rb b/lib/optimizely/config_manager/static_project_config_manager.rb index 281beb3d..38829ce4 100644 --- a/lib/optimizely/config_manager/static_project_config_manager.rb +++ b/lib/optimizely/config_manager/static_project_config_manager.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # -# Copyright 2019-2020, 2022, Optimizely and contributors +# Copyright 2019-2020, 2022-2023, Optimizely and contributors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -23,7 +23,7 @@ module Optimizely class StaticProjectConfigManager < ProjectConfigManager # Implementation of ProjectConfigManager interface. - attr_reader :config + attr_reader :config, :sdk_key def initialize(datafile, logger, error_handler, skip_json_validation) # Looks up and sets datafile and config based on response body. @@ -41,6 +41,7 @@ def initialize(datafile, logger, error_handler, skip_json_validation) error_handler, skip_json_validation ) + @sdk_key = @config&.sdk_key @optimizely_config = nil end diff --git a/lib/optimizely/exceptions.rb b/lib/optimizely/exceptions.rb index 51cb5098..50ef62c0 100644 --- a/lib/optimizely/exceptions.rb +++ b/lib/optimizely/exceptions.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # -# Copyright 2016-2020, 2022, Optimizely and contributors +# Copyright 2016-2020, 2022-2023, Optimizely and contributors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -32,6 +32,13 @@ def initialize(msg = 'Provided URI was invalid.') end end + class MissingSdkKeyError < Error + # Raised when a provided URI is invalid. + def initialize(msg = 'SDK key not provided/cannot be found in the datafile.') + super + end + end + class InvalidAudienceError < Error # Raised when an invalid audience is provided diff --git a/lib/optimizely/notification_center_registry.rb b/lib/optimizely/notification_center_registry.rb new file mode 100644 index 00000000..aea0ade0 --- /dev/null +++ b/lib/optimizely/notification_center_registry.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +# +# Copyright 2023, Optimizely and contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +require_relative 'notification_center' +require_relative 'exceptions' + +module Optimizely + class NotificationCenterRegistry + private_class_method :new + # Class managing internal notification centers. + # @api no-doc + @notification_centers = {} + @mutex = Mutex.new + + # Returns an internal notification center for the given sdk_key, creating one + # if none exists yet. + # + # Args: + # sdk_key: A string sdk key to uniquely identify the notification center. + # logger: Optional logger. + + # Returns: + # nil or NotificationCenter + def self.get_notification_center(sdk_key, logger) + unless sdk_key + logger&.log(Logger::ERROR, "#{MissingSdkKeyError.new.message} ODP may not work properly without it.") + return nil + end + + notification_center = nil + + @mutex.synchronize do + if @notification_centers.key?(sdk_key) + notification_center = @notification_centers[sdk_key] + else + notification_center = NotificationCenter.new(logger, nil) + @notification_centers[sdk_key] = notification_center + end + end + + notification_center + end + + # Remove a previously added notification center and clear all its listeners. + + # Args: + # sdk_key: The sdk_key of the notification center to remove. + def self.remove_notification_center(sdk_key) + @mutex.synchronize do + @notification_centers + .delete(sdk_key) + &.clear_all_notification_listeners + end + nil + end + end +end diff --git a/spec/config_manager/http_project_config_manager_spec.rb b/spec/config_manager/http_project_config_manager_spec.rb index c786f38e..3c048e9f 100644 --- a/spec/config_manager/http_project_config_manager_spec.rb +++ b/spec/config_manager/http_project_config_manager_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # -# Copyright 2019-2020, 2022, Optimizely and contributors +# Copyright 2019-2020, 2022-2023, Optimizely and contributors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -57,6 +57,7 @@ describe '.project_config_manager' do it 'should get project config when valid url is given' do @http_project_config_manager = Optimizely::HTTPProjectConfigManager.new( + sdk_key: 'valid_sdk_key', url: 'https://cdn.optimizely.com/datafiles/valid_sdk_key.json' ) @@ -75,6 +76,7 @@ .to_return(status: 200, body: VALID_SDK_KEY_CONFIG_JSON, headers: {}) @http_project_config_manager = Optimizely::HTTPProjectConfigManager.new( + sdk_key: 'valid_sdk_key', url: 'http://cdn.optimizely.com/datafiles/valid_sdk_key.json' ) diff --git a/spec/notification_center_registry_spec.rb b/spec/notification_center_registry_spec.rb new file mode 100644 index 00000000..5a691059 --- /dev/null +++ b/spec/notification_center_registry_spec.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +# +# Copyright 2017-2019, 2022-2023, Optimizely and contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +require 'spec_helper' +require 'optimizely/error_handler' +require 'optimizely/event_builder' +require 'optimizely/exceptions' +require 'optimizely/logger' +require 'optimizely/notification_center' +require 'optimizely/notification_center_registry' +describe Optimizely::NotificationCenter do + let(:spy_logger) { spy('logger') } + let(:config_body) { OptimizelySpec::VALID_CONFIG_BODY } + let(:config_body_JSON) { OptimizelySpec::VALID_CONFIG_BODY_JSON } + let(:error_handler) { Optimizely::NoOpErrorHandler.new } + let(:logger) { Optimizely::NoOpLogger.new } + let(:notification_center) { Optimizely::NotificationCenter.new(spy_logger, error_handler) } + + describe '#NotificationCenterRegistry' do + describe 'test get notification center' do + it 'should log error with no sdk_key' do + Optimizely::NotificationCenterRegistry.get_notification_center(nil, spy_logger) + expect(spy_logger).to have_received(:log).with(Logger::ERROR, "#{Optimizely::MissingSdkKeyError.new.message} ODP may not work properly without it.") + end + + it 'should return notification center with odp callback' do + sdk_key = 'VALID' + stub_request(:get, "https://cdn.optimizely.com/datafiles/#{sdk_key}.json") + .to_return(status: 200, body: config_body_JSON) + + project = Optimizely::Project.new(nil, nil, spy_logger, nil, false, nil, sdk_key) + + notification_center = Optimizely::NotificationCenterRegistry.get_notification_center(sdk_key, spy_logger) + expect(notification_center).to be_a Optimizely::NotificationCenter + + config_notifications = notification_center.instance_variable_get('@notifications')[Optimizely::NotificationCenter::NOTIFICATION_TYPES[:OPTIMIZELY_CONFIG_UPDATE]] + expect(config_notifications).to include({notification_id: anything, callback: project.method(:update_odp_config_on_datafile_update)}) + expect(spy_logger).not_to have_received(:log).with(Logger::ERROR, anything) + + project.close + end + + it 'should only create one notification center per sdk_key' do + sdk_key = 'single' + stub_request(:get, "https://cdn.optimizely.com/datafiles/#{sdk_key}.json") + .to_return(status: 200, body: config_body_JSON) + + notification_center = Optimizely::NotificationCenterRegistry.get_notification_center(sdk_key, spy_logger) + project = Optimizely::Project.new(nil, nil, spy_logger, nil, false, nil, sdk_key) + + expect(notification_center).to eq(Optimizely::NotificationCenterRegistry.get_notification_center(sdk_key, spy_logger)) + expect(spy_logger).not_to have_received(:log).with(Logger::ERROR, anything) + + project.close + end + end + + describe 'test remove notification center' do + it 'should remove notification center and callbacks' do + sdk_key = 'segments-test' + stub_request(:get, "https://cdn.optimizely.com/datafiles/#{sdk_key}.json") + .to_return(status: 200, body: config_body_JSON) + + notification_center = Optimizely::NotificationCenterRegistry.get_notification_center(sdk_key, spy_logger) + expect(notification_center).to receive(:send_notifications).once + + project = Optimizely::Project.new(nil, nil, spy_logger, nil, false, nil, sdk_key) + project.config_manager.config + + Optimizely::NotificationCenterRegistry.remove_notification_center(sdk_key) + expect(Optimizely::NotificationCenterRegistry.instance_variable_get('@notification_centers').values).not_to include(notification_center) + + revised_datafile = config_body.dup + revised_datafile['revision'] = (revised_datafile['revision'].to_i + 1).to_s + revised_datafile = Optimizely::DatafileProjectConfig.create(JSON.dump(revised_datafile), spy_logger, nil, nil) + + # trigger notification + project.config_manager.send(:set_config, revised_datafile) + expect(notification_center).not_to receive(:send_notifications) + expect(notification_center).not_to eq(Optimizely::NotificationCenterRegistry.get_notification_center(sdk_key, spy_logger)) + + expect(spy_logger).not_to have_received(:log).with(Logger::ERROR, anything) + + project.close + end + end + end +end diff --git a/spec/notification_center_spec.rb b/spec/notification_center_spec.rb index 978de5ac..7ac4e808 100644 --- a/spec/notification_center_spec.rb +++ b/spec/notification_center_spec.rb @@ -313,7 +313,7 @@ def call; end notification_type = Optimizely::NotificationCenter::NOTIFICATION_TYPES[:ACTIVATE] @inner_notification_center.clear_notification_listeners(notification_type) expect { @inner_notification_center.clear_notification_listeners(notification_type) } - .to_not raise_error(Optimizely::InvalidNotificationType) + .to_not raise_error expect( @inner_notification_center.notifications[ Optimizely::NotificationCenter::NOTIFICATION_TYPES[:ACTIVATE] diff --git a/spec/optimizely_factory_spec.rb b/spec/optimizely_factory_spec.rb index c875fec1..65c8d4d5 100644 --- a/spec/optimizely_factory_spec.rb +++ b/spec/optimizely_factory_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # -# Copyright 2019, 2022, Optimizely and contributors +# Copyright 2019, 2022-2023, Optimizely and contributors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -70,7 +70,7 @@ describe '.default_instance_with_manager' do it 'should take provided custom config manager' do class CustomConfigManager # rubocop:disable Lint/ConstantDefinitionInBlock - attr_reader :config + attr_reader :config, :sdk_key end custom_config_manager = CustomConfigManager.new diff --git a/spec/optimizely_user_context_spec.rb b/spec/optimizely_user_context_spec.rb index 88a915a7..0f2a3d3c 100644 --- a/spec/optimizely_user_context_spec.rb +++ b/spec/optimizely_user_context_spec.rb @@ -382,7 +382,7 @@ expect(decision.user_context.forced_decisions).to eq(context => forced_decision) expect(decision.reasons).to eq(['Variation (3324490633) is mapped to flag (feature_1), rule (exp_with_audience) and user (tester) in the forced decision map.']) end - expected.to raise_error + expected.to raise_error Optimizely::InvalidVariationError end it 'should return correct variation if rule in forced decision is deleted' do diff --git a/spec/project_spec.rb b/spec/project_spec.rb index b5690e06..36ca3363 100644 --- a/spec/project_spec.rb +++ b/spec/project_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # -# Copyright 2016-2020, 2022, Optimizely and contributors +# Copyright 2016-2020, 2022-2023, Optimizely and contributors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -28,11 +28,21 @@ require 'optimizely/version' describe 'Optimizely' do - let(:config_body) { OptimizelySpec::VALID_CONFIG_BODY } - let(:config_body_JSON) { OptimizelySpec::VALID_CONFIG_BODY_JSON } + # need different sdk_key for every instance, otherwise notification center callbacks get called for the wrong tests + let!(:sdk_key) { SecureRandom.uuid } + let(:config_body) do + datafile = OptimizelySpec::VALID_CONFIG_BODY.dup + datafile['sdkKey'] = sdk_key + datafile + end + let(:config_body_JSON) { JSON.dump(config_body) } let(:config_body_invalid_JSON) { OptimizelySpec::INVALID_CONFIG_BODY_JSON } - let(:config_body_integrations) { OptimizelySpec::CONFIG_DICT_WITH_INTEGRATIONS } - let(:config_body_integrations_JSON) { OptimizelySpec::CONFIG_DICT_WITH_INTEGRATIONS_JSON } + let(:config_body_integrations) do + datafile = OptimizelySpec::CONFIG_DICT_WITH_INTEGRATIONS.dup + datafile['sdkKey'] = sdk_key + datafile + end + let(:config_body_integrations_JSON) { JSON.dump(config_body_integrations) } let(:error_handler) { Optimizely::RaiseErrorHandler.new } let(:spy_logger) { spy('logger') } let(:version) { Optimizely::VERSION } @@ -134,7 +144,7 @@ class InvalidErrorHandler; end # rubocop:disable Lint/ConstantDefinitionInBlock allow_any_instance_of(Optimizely::SimpleLogger).to receive(:log).with(Logger::INFO, anything) allow_any_instance_of(Optimizely::SimpleLogger).to receive(:log).with(Logger::DEBUG, anything) expect_any_instance_of(Optimizely::SimpleLogger).to receive(:log).once.with(Logger::ERROR, 'Provided datafile is in an invalid format.') - config = config_body_integrations.dup + config = OptimizelySpec.deep_clone(config_body_integrations) config['integrations'][0].delete('key') integrations_json = JSON.dump(config) @@ -142,7 +152,7 @@ class InvalidErrorHandler; end # rubocop:disable Lint/ConstantDefinitionInBlock end it 'should be valid when datafile contains integrations with only key' do - config = config_body_integrations.dup + config = OptimizelySpec.deep_clone(config_body_integrations) config['integrations'].clear config['integrations'].push('key' => '123') integrations_json = JSON.dump(config) @@ -152,7 +162,7 @@ class InvalidErrorHandler; end # rubocop:disable Lint/ConstantDefinitionInBlock end it 'should be valid when datafile contains integrations with arbitrary fields' do - config = config_body_integrations.dup + config = OptimizelySpec.deep_clone(config_body_integrations) config['integrations'].clear config['integrations'].push('key' => 'future', 'any-key-1' => 1, 'any-key-2' => 'any-value-2') integrations_json = JSON.dump(config) @@ -881,7 +891,7 @@ def callback(_args); end describe '.Optimizely with config manager' do before(:example) do stub_request(:post, impression_log_url) - stub_request(:get, 'https://cdn.optimizely.com/datafiles/valid_sdk_key.json') + stub_request(:get, "https://cdn.optimizely.com/datafiles/#{sdk_key}.json") .with( headers: { 'Content-Type' => 'application/json' @@ -901,7 +911,8 @@ def callback(_args); end expect(notification_center).to receive(:send_notifications).ordered http_project_config_manager = Optimizely::HTTPProjectConfigManager.new( - url: 'https://cdn.optimizely.com/datafiles/valid_sdk_key.json', + sdk_key: sdk_key, + url: "https://cdn.optimizely.com/datafiles/#{sdk_key}.json", notification_center: notification_center ) @@ -928,7 +939,7 @@ def callback(_args); end expect(notification_center).to receive(:send_notifications).ordered http_project_config_manager = Optimizely::HTTPProjectConfigManager.new( - sdk_key: 'valid_sdk_key', + sdk_key: sdk_key, notification_center: notification_center ) @@ -948,7 +959,7 @@ def callback(_args); end describe '.Optimizely with sdk key' do before(:example) do stub_request(:post, impression_log_url) - stub_request(:get, 'https://cdn.optimizely.com/datafiles/valid_sdk_key.json') + stub_request(:get, "https://cdn.optimizely.com/datafiles/#{sdk_key}.json") .with( headers: { 'Content-Type' => 'application/json' @@ -968,7 +979,7 @@ def callback(_args); end custom_project_instance = Optimizely::Project.new( nil, nil, spy_logger, error_handler, - false, nil, 'valid_sdk_key', nil, notification_center + false, nil, sdk_key, nil, notification_center ) sleep 0.1 until custom_project_instance.config_manager.ready? @@ -3455,7 +3466,7 @@ def callback(_args); end describe '.close' do before(:example) do stub_request(:post, impression_log_url) - stub_request(:get, 'https://cdn.optimizely.com/datafiles/valid_sdk_key.json') + stub_request(:get, "https://cdn.optimizely.com/datafiles/#{sdk_key}.json") .with( headers: { 'Content-Type' => 'application/json' @@ -3466,7 +3477,7 @@ def callback(_args); end it 'should stop config manager and event processor when optimizely close is called' do config_manager = Optimizely::HTTPProjectConfigManager.new( - sdk_key: 'valid_sdk_key', + sdk_key: sdk_key, start_by_default: true ) @@ -3490,7 +3501,7 @@ def callback(_args); end it 'should stop invalid object' do http_project_config_manager = Optimizely::HTTPProjectConfigManager.new( - sdk_key: 'valid_sdk_key' + sdk_key: sdk_key ) project_instance = Optimizely::Project.new( @@ -3504,7 +3515,7 @@ def callback(_args); end it 'shoud return optimizely as invalid for an API when close is called' do http_project_config_manager = Optimizely::HTTPProjectConfigManager.new( - sdk_key: 'valid_sdk_key' + sdk_key: sdk_key ) project_instance = Optimizely::Project.new( @@ -4440,10 +4451,10 @@ def callback(_args); end describe 'sdk_settings' do it 'should log info when disabled' do project_instance.close - stub_request(:get, 'https://cdn.optimizely.com/datafiles/sdk-key.json') + stub_request(:get, "https://cdn.optimizely.com/datafiles/#{sdk_key}.json") .to_return(status: 200, body: config_body_integrations_JSON) sdk_settings = Optimizely::Helpers::OptimizelySdkSettings.new(disable_odp: true) - project = Optimizely::Project.new(nil, nil, spy_logger, error_handler, false, nil, 'sdk-key', nil, nil, nil, [], sdk_settings) + project = Optimizely::Project.new(nil, nil, spy_logger, error_handler, false, nil, sdk_key, nil, nil, nil, [], sdk_settings) expect(project.odp_manager.instance_variable_get('@event_manager')).to be_nil expect(project.odp_manager.instance_variable_get('@segment_manager')).to be_nil project.close @@ -4453,11 +4464,11 @@ def callback(_args); end end it 'should accept cache_size' do - stub_request(:get, 'https://cdn.optimizely.com/datafiles/sdk-key.json') + stub_request(:get, "https://cdn.optimizely.com/datafiles/#{sdk_key}.json") .to_return(status: 200, body: config_body_integrations_JSON) sdk_settings = Optimizely::Helpers::OptimizelySdkSettings.new(segments_cache_size: 5) - project = Optimizely::Project.new(nil, nil, spy_logger, error_handler, false, nil, 'sdk-key', nil, nil, nil, [], sdk_settings) + project = Optimizely::Project.new(nil, nil, spy_logger, error_handler, false, nil, sdk_key, nil, nil, nil, [], sdk_settings) segment_manager = project.odp_manager.instance_variable_get('@segment_manager') expect(segment_manager.instance_variable_get('@segments_cache').capacity).to eq 5 project.close @@ -4466,10 +4477,10 @@ def callback(_args); end end it 'should accept cache_timeout' do - stub_request(:get, 'https://cdn.optimizely.com/datafiles/sdk-key.json') + stub_request(:get, "https://cdn.optimizely.com/datafiles/#{sdk_key}.json") .to_return(status: 200, body: config_body_integrations_JSON) sdk_settings = Optimizely::Helpers::OptimizelySdkSettings.new(segments_cache_timeout_in_secs: 5) - project = Optimizely::Project.new(nil, nil, spy_logger, error_handler, false, nil, 'sdk-key', nil, nil, nil, [], sdk_settings) + project = Optimizely::Project.new(nil, nil, spy_logger, error_handler, false, nil, sdk_key, nil, nil, nil, [], sdk_settings) segment_manager = project.odp_manager.instance_variable_get('@segment_manager') expect(segment_manager.instance_variable_get('@segments_cache').timeout).to eq 5 project.close @@ -4478,10 +4489,10 @@ def callback(_args); end end it 'should accept cache_size and cache_timeout' do - stub_request(:get, 'https://cdn.optimizely.com/datafiles/sdk-key.json') + stub_request(:get, "https://cdn.optimizely.com/datafiles/#{sdk_key}.json") .to_return(status: 200, body: config_body_integrations_JSON) sdk_settings = Optimizely::Helpers::OptimizelySdkSettings.new(segments_cache_size: 10, segments_cache_timeout_in_secs: 5) - project = Optimizely::Project.new(nil, nil, spy_logger, error_handler, false, nil, 'sdk-key', nil, nil, nil, [], sdk_settings) + project = Optimizely::Project.new(nil, nil, spy_logger, error_handler, false, nil, sdk_key, nil, nil, nil, [], sdk_settings) segment_manager = project.odp_manager.instance_variable_get('@segment_manager') segments_cache = segment_manager.instance_variable_get('@segments_cache') expect(segments_cache.capacity).to eq 10 @@ -4498,10 +4509,10 @@ def lookup(key); end def save(key, value); end end - stub_request(:get, 'https://cdn.optimizely.com/datafiles/sdk-key.json') + stub_request(:get, "https://cdn.optimizely.com/datafiles/#{sdk_key}.json") .to_return(status: 200, body: config_body_integrations_JSON) sdk_settings = Optimizely::Helpers::OptimizelySdkSettings.new(odp_segments_cache: CustomCache.new) - project = Optimizely::Project.new(nil, nil, spy_logger, error_handler, false, nil, 'sdk-key', nil, nil, nil, [], sdk_settings) + project = Optimizely::Project.new(nil, nil, spy_logger, error_handler, false, nil, sdk_key, nil, nil, nil, [], sdk_settings) segment_manager = project.odp_manager.instance_variable_get('@segment_manager') expect(segment_manager.instance_variable_get('@segments_cache')).to be_a CustomCache project.close @@ -4613,15 +4624,17 @@ class InvalidEventManager; end # rubocop:disable Lint/ConstantDefinitionInBlock end it 'should send event with HTTPProjectConfigManager' do - stub_request(:get, 'https://cdn.optimizely.com/datafiles/sdk-key.json') - .to_return(status: 200, body: config_body_integrations_JSON) + datafile = OptimizelySpec.deep_clone(config_body_integrations) + stub_request(:get, "https://cdn.optimizely.com/datafiles/#{sdk_key}.json") + .to_return(status: 200, body: JSON.dump(datafile)) stub_request(:post, 'https://api.zaius.com/v3/events').to_return(status: 200) expect(spy_logger).to receive(:log).once.with(Logger::DEBUG, 'ODP event queue: flushing batch size 1.') expect(spy_logger).not_to receive(:log).with(Logger::ERROR, anything) - project = Optimizely::Project.new(nil, nil, spy_logger, nil, false, nil, 'sdk-key') + project = Optimizely::Project.new(nil, nil, spy_logger, nil, false, nil, sdk_key) # wait until project_config ready project.send(:project_config) + sleep 0.1 until project.odp_manager.instance_variable_get('@event_manager').instance_variable_get('@event_queue').empty? project.send_odp_event(type: 'wow', action: 'great', identifiers: {}, data: {}) project.close @@ -4637,17 +4650,17 @@ class InvalidEventManager; end # rubocop:disable Lint/ConstantDefinitionInBlock it 'should log debug if datafile not ready' do expect(spy_logger).to receive(:log).once.with(Logger::DEBUG, 'ODP event queue: cannot send before config has been set.') - project = Optimizely::Project.new(nil, nil, spy_logger, nil, false, nil, 'sdk-key') + project = Optimizely::Project.new(nil, nil, spy_logger, nil, false, nil, sdk_key) project.send_odp_event(type: 'wow', action: 'great', identifiers: {}, data: {}) project.close end it 'should log error if odp not enabled with HTTPProjectConfigManager' do - stub_request(:get, 'https://cdn.optimizely.com/datafiles/sdk-key.json') + stub_request(:get, "https://cdn.optimizely.com/datafiles/#{sdk_key}.json") .to_return(status: 200, body: config_body_integrations_JSON) expect(spy_logger).to receive(:log).once.with(Logger::ERROR, 'ODP is not enabled.') sdk_settings = Optimizely::Helpers::OptimizelySdkSettings.new(disable_odp: true) - project = Optimizely::Project.new(nil, nil, spy_logger, error_handler, false, nil, 'sdk-key', nil, nil, nil, [], sdk_settings) + project = Optimizely::Project.new(nil, nil, spy_logger, error_handler, false, nil, sdk_key, nil, nil, nil, [], sdk_settings) sleep 0.1 until project.config_manager.ready? project.send_odp_event(type: 'wow', action: 'great', identifiers: {}, data: {}) project.close diff --git a/spec/spec_params.rb b/spec/spec_params.rb index 1e5911dd..e43ce3cc 100644 --- a/spec/spec_params.rb +++ b/spec/spec_params.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # -# Copyright 2016-2021, Optimizely and contributors +# Copyright 2016-2021, 2023, Optimizely and contributors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -1328,7 +1328,8 @@ module OptimizelySpec 'key' => 'event1' } ], - 'revision' => '101' + 'revision' => '101', + 'sdkKey' => 'INTEGRATIONS' }.freeze SIMILAR_EXP_KEYS = { @@ -1936,4 +1937,21 @@ module OptimizelySpec # SEND_FLAG_DECISIONS_DISABLED_CONFIG['sendFlagDecisions'] = false CONFIG_DICT_WITH_INTEGRATIONS_JSON = JSON.dump(CONFIG_DICT_WITH_INTEGRATIONS) + + def self.deep_clone(obj) + obj.dup.tap do |new_obj| + case new_obj + when Hash + new_obj.each do |key, val| + new_obj[key] = deep_clone(val) + end + when Array + new_obj.map! do |val| + deep_clone(val) + end + else + new_obj + end + end + end end