From d32ca1e9f7949e34f3237f796fff865e6480f94c Mon Sep 17 00:00:00 2001 From: Peter Adam Korodi <52385411+kp-cat@users.noreply.github.com> Date: Fri, 8 Mar 2024 09:38:14 +0100 Subject: [PATCH] config v6 support (#32) * config v6 support * test data * evaluation log tests (no rest only test) * tests * rollout tests * lint fix * sonarcloud * fix tests on ruby 2.7 + add ruby 3.2, 3.3 to the CI * compile fix on ruby 3.2 * extend_config_with_inline_salt_and_segment > fixup_config_salt_and_segments * review fixes --- .github/workflows/ruby-ci.yml | 3 +- .github/workflows/snyk.yml | 2 +- .sonarcloud.properties | 2 + lib/configcat/config.rb | 317 ++++++ lib/configcat/configcatclient.rb | 152 ++- lib/configcat/configcatlogger.rb | 4 + lib/configcat/configentry.rb | 1 + lib/configcat/configfetcher.rb | 11 +- lib/configcat/configservice.rb | 56 +- lib/configcat/constants.rb | 18 - lib/configcat/evaluationcontext.rb | 14 + lib/configcat/evaluationdetails.rb | 26 +- lib/configcat/evaluationlogbuilder.rb | 81 ++ lib/configcat/localdictionarydatasource.rb | 24 +- lib/configcat/localfiledatasource.rb | 30 +- lib/configcat/rolloutevaluator.rb | 865 ++++++++++++--- lib/configcat/user.rb | 47 +- lib/configcat/utils.rb | 22 +- lib/configcat/version.rb | 2 +- spec/config_spec.rb | 42 + spec/configcat/autopollingcachepolicy_spec.rb | 93 +- spec/configcat/configcache_spec.rb | 18 +- spec/configcat/configcatclient_spec.rb | 250 ++++- spec/configcat/configfetcher_spec.rb | 12 +- spec/configcat/hooks_spec.rb | 27 +- spec/configcat/lazyloadingcachepolicy_spec.rb | 126 ++- .../manualpollingcachepolicy_spec.rb | 51 +- spec/configcat/mocks.rb | 85 +- spec/configcat/user_spec.rb | 12 +- .../data/comparison_attribute_conversion.json | 789 ++++++++++++++ spec/data/comparison_attribute_trimming.json | 985 ++++++++++++++++++ spec/data/comparison_value_trimming.json | 777 ++++++++++++++ spec/data/evaluation/1_targeting_rule.json | 41 + .../1_rule_matching_targeted_attribute.txt | 4 + .../1_rule_no_targeted_attribute.txt | 6 + .../1_targeting_rule/1_rule_no_user.txt | 6 + ...1_rule_not_matching_targeted_attribute.txt | 4 + spec/data/evaluation/2_targeting_rules.json | 41 + .../2_rules_matching_targeted_attribute.txt | 7 + .../2_rules_no_targeted_attribute.txt | 9 + .../2_targeting_rules/2_rules_no_user.txt | 8 + ..._rules_not_matching_targeted_attribute.txt | 7 + spec/data/evaluation/and_rules.json | 22 + .../and_rules/and_rules_no_user.txt | 7 + .../evaluation/and_rules/and_rules_user.txt | 7 + spec/data/evaluation/comparators.json | 20 + spec/data/evaluation/comparators/allinone.txt | 57 + .../evaluation/epoch_date_validation.json | 16 + .../epoch_date_validation/date_error.txt | 7 + spec/data/evaluation/list_truncation.json | 14 + .../list_truncation/list_truncation.txt | 7 + .../list_truncation/test_list_truncation.json | 83 ++ spec/data/evaluation/number_validation.json | 16 + .../number_validation/number_error.txt | 6 + .../options_after_targeting_rule.json | 41 + ...eting_rule_matching_targeted_attribute.txt | 4 + ...r_targeting_rule_no_targeted_attribute.txt | 9 + .../options_after_targeting_rule_no_user.txt | 7 + ...g_rule_not_matching_targeted_attribute.txt | 7 + .../options_based_on_custom_attr.json | 31 + .../matching_options_custom_attribute.txt | 5 + .../no_options_custom_attribute.txt | 4 + .../options_custom_attribute_no_user.txt | 4 + .../evaluation/options_based_on_user_id.json | 21 + .../options_user_attribute_no_user.txt | 4 + .../options_user_attribute_user.txt | 5 + .../options_within_targeting_rule.json | 52 + ...argeted_attribute_no_options_attribute.txt | 7 + ...g_targeted_attribute_options_attribute.txt | 7 + ...n_targeting_rule_no_targeted_attribute.txt | 6 + .../options_within_targeting_rule_no_user.txt | 6 + ...g_rule_not_matching_targeted_attribute.txt | 4 + spec/data/evaluation/prerequisite_flag.json | 41 + .../prerequisite_flag/prerequisite_flag.txt | 32 + .../prerequisite_flag_multilevel.txt | 24 + ...erequisite_flag_no_user_needed_by_both.txt | 38 + ...rerequisite_flag_no_user_needed_by_dep.txt | 15 + ...equisite_flag_no_user_needed_by_prereq.txt | 18 + spec/data/evaluation/segment.json | 47 + .../evaluation/segment/segment_matching.txt | 11 + .../segment/segment_no_matching.txt | 11 + .../segment/segment_no_targeted_attribute.txt | 13 + .../evaluation/segment/segment_no_user.txt | 6 + .../segment_no_user_multi_conditions.txt | 7 + spec/data/evaluation/semver_validation.json | 26 + .../semver_validation/semver_error.txt | 9 + .../semver_relations_error.txt | 18 + spec/data/evaluation/simple_value.json | 37 + .../simple_value/double_setting.txt | 2 + .../evaluation/simple_value/int_setting.txt | 2 + .../data/evaluation/simple_value/off_flag.txt | 2 + spec/data/evaluation/simple_value/on_flag.txt | 2 + .../evaluation/simple_value/text_setting.txt | 2 + spec/{ => data}/test-simple.json | 0 spec/data/test.json | 24 + spec/data/test_circulardependency_v6.json | 80 ++ .../data/test_override_flagdependency_v6.json | 44 + spec/data/test_override_segments_v6.json | 66 ++ spec/{ => data}/testmatrix.csv | 0 spec/data/testmatrix_and_or.csv | 15 + spec/data/testmatrix_comparators_v6.csv | 24 + spec/{ => data}/testmatrix_number.csv | 0 spec/data/testmatrix_prerequisite_flag.csv | 5 + spec/data/testmatrix_segments.csv | 6 + spec/data/testmatrix_segments_old.csv | 6 + spec/{ => data}/testmatrix_semantic.csv | 0 spec/{ => data}/testmatrix_semantic_2.csv | 0 spec/{ => data}/testmatrix_sensitive.csv | 0 spec/data/testmatrix_unicode.csv | 14 + spec/{ => data}/testmatrix_variationId.csv | 0 spec/evaluationlog_spec.rb | 137 +++ spec/integration_spec.rb | 3 +- spec/override_spec.rb | 103 +- spec/rollout_spec.rb | 484 ++++++++- spec/specialcharacter_spec.rb | 22 + spec/test.json | 19 - 116 files changed, 6437 insertions(+), 541 deletions(-) create mode 100644 .sonarcloud.properties create mode 100644 lib/configcat/config.rb delete mode 100644 lib/configcat/constants.rb create mode 100644 lib/configcat/evaluationcontext.rb create mode 100644 lib/configcat/evaluationlogbuilder.rb create mode 100644 spec/config_spec.rb create mode 100644 spec/data/comparison_attribute_conversion.json create mode 100644 spec/data/comparison_attribute_trimming.json create mode 100644 spec/data/comparison_value_trimming.json create mode 100644 spec/data/evaluation/1_targeting_rule.json create mode 100644 spec/data/evaluation/1_targeting_rule/1_rule_matching_targeted_attribute.txt create mode 100644 spec/data/evaluation/1_targeting_rule/1_rule_no_targeted_attribute.txt create mode 100644 spec/data/evaluation/1_targeting_rule/1_rule_no_user.txt create mode 100644 spec/data/evaluation/1_targeting_rule/1_rule_not_matching_targeted_attribute.txt create mode 100644 spec/data/evaluation/2_targeting_rules.json create mode 100644 spec/data/evaluation/2_targeting_rules/2_rules_matching_targeted_attribute.txt create mode 100644 spec/data/evaluation/2_targeting_rules/2_rules_no_targeted_attribute.txt create mode 100644 spec/data/evaluation/2_targeting_rules/2_rules_no_user.txt create mode 100644 spec/data/evaluation/2_targeting_rules/2_rules_not_matching_targeted_attribute.txt create mode 100644 spec/data/evaluation/and_rules.json create mode 100644 spec/data/evaluation/and_rules/and_rules_no_user.txt create mode 100644 spec/data/evaluation/and_rules/and_rules_user.txt create mode 100644 spec/data/evaluation/comparators.json create mode 100644 spec/data/evaluation/comparators/allinone.txt create mode 100644 spec/data/evaluation/epoch_date_validation.json create mode 100644 spec/data/evaluation/epoch_date_validation/date_error.txt create mode 100644 spec/data/evaluation/list_truncation.json create mode 100644 spec/data/evaluation/list_truncation/list_truncation.txt create mode 100644 spec/data/evaluation/list_truncation/test_list_truncation.json create mode 100644 spec/data/evaluation/number_validation.json create mode 100644 spec/data/evaluation/number_validation/number_error.txt create mode 100644 spec/data/evaluation/options_after_targeting_rule.json create mode 100644 spec/data/evaluation/options_after_targeting_rule/options_after_targeting_rule_matching_targeted_attribute.txt create mode 100644 spec/data/evaluation/options_after_targeting_rule/options_after_targeting_rule_no_targeted_attribute.txt create mode 100644 spec/data/evaluation/options_after_targeting_rule/options_after_targeting_rule_no_user.txt create mode 100644 spec/data/evaluation/options_after_targeting_rule/options_after_targeting_rule_not_matching_targeted_attribute.txt create mode 100644 spec/data/evaluation/options_based_on_custom_attr.json create mode 100644 spec/data/evaluation/options_based_on_custom_attr/matching_options_custom_attribute.txt create mode 100644 spec/data/evaluation/options_based_on_custom_attr/no_options_custom_attribute.txt create mode 100644 spec/data/evaluation/options_based_on_custom_attr/options_custom_attribute_no_user.txt create mode 100644 spec/data/evaluation/options_based_on_user_id.json create mode 100644 spec/data/evaluation/options_based_on_user_id/options_user_attribute_no_user.txt create mode 100644 spec/data/evaluation/options_based_on_user_id/options_user_attribute_user.txt create mode 100644 spec/data/evaluation/options_within_targeting_rule.json create mode 100644 spec/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_no_options_attribute.txt create mode 100644 spec/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_options_attribute.txt create mode 100644 spec/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_no_targeted_attribute.txt create mode 100644 spec/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_no_user.txt create mode 100644 spec/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_not_matching_targeted_attribute.txt create mode 100644 spec/data/evaluation/prerequisite_flag.json create mode 100644 spec/data/evaluation/prerequisite_flag/prerequisite_flag.txt create mode 100644 spec/data/evaluation/prerequisite_flag/prerequisite_flag_multilevel.txt create mode 100644 spec/data/evaluation/prerequisite_flag/prerequisite_flag_no_user_needed_by_both.txt create mode 100644 spec/data/evaluation/prerequisite_flag/prerequisite_flag_no_user_needed_by_dep.txt create mode 100644 spec/data/evaluation/prerequisite_flag/prerequisite_flag_no_user_needed_by_prereq.txt create mode 100644 spec/data/evaluation/segment.json create mode 100644 spec/data/evaluation/segment/segment_matching.txt create mode 100644 spec/data/evaluation/segment/segment_no_matching.txt create mode 100644 spec/data/evaluation/segment/segment_no_targeted_attribute.txt create mode 100644 spec/data/evaluation/segment/segment_no_user.txt create mode 100644 spec/data/evaluation/segment/segment_no_user_multi_conditions.txt create mode 100644 spec/data/evaluation/semver_validation.json create mode 100644 spec/data/evaluation/semver_validation/semver_error.txt create mode 100644 spec/data/evaluation/semver_validation/semver_relations_error.txt create mode 100644 spec/data/evaluation/simple_value.json create mode 100644 spec/data/evaluation/simple_value/double_setting.txt create mode 100644 spec/data/evaluation/simple_value/int_setting.txt create mode 100644 spec/data/evaluation/simple_value/off_flag.txt create mode 100644 spec/data/evaluation/simple_value/on_flag.txt create mode 100644 spec/data/evaluation/simple_value/text_setting.txt rename spec/{ => data}/test-simple.json (100%) create mode 100644 spec/data/test.json create mode 100644 spec/data/test_circulardependency_v6.json create mode 100644 spec/data/test_override_flagdependency_v6.json create mode 100644 spec/data/test_override_segments_v6.json rename spec/{ => data}/testmatrix.csv (100%) create mode 100644 spec/data/testmatrix_and_or.csv create mode 100644 spec/data/testmatrix_comparators_v6.csv rename spec/{ => data}/testmatrix_number.csv (100%) create mode 100644 spec/data/testmatrix_prerequisite_flag.csv create mode 100644 spec/data/testmatrix_segments.csv create mode 100644 spec/data/testmatrix_segments_old.csv rename spec/{ => data}/testmatrix_semantic.csv (100%) rename spec/{ => data}/testmatrix_semantic_2.csv (100%) rename spec/{ => data}/testmatrix_sensitive.csv (100%) create mode 100644 spec/data/testmatrix_unicode.csv rename spec/{ => data}/testmatrix_variationId.csv (100%) create mode 100644 spec/evaluationlog_spec.rb create mode 100644 spec/specialcharacter_spec.rb delete mode 100644 spec/test.json diff --git a/.github/workflows/ruby-ci.yml b/.github/workflows/ruby-ci.yml index 7f69c7f..551c7b1 100644 --- a/.github/workflows/ruby-ci.yml +++ b/.github/workflows/ruby-ci.yml @@ -16,7 +16,8 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - ruby-versions: [ 2.4, 2.5, 2.6, 2.7, '3.0', '3.1' ] + ruby-versions: [ '2.4', '2.5', '2.6', '2.7', '3.0', '3.1', '3.2', '3.3' ] + steps: - uses: actions/checkout@v3 - name: Set up Ruby diff --git a/.github/workflows/snyk.yml b/.github/workflows/snyk.yml index 69196c9..fb042b9 100644 --- a/.github/workflows/snyk.yml +++ b/.github/workflows/snyk.yml @@ -12,7 +12,7 @@ jobs: snyk: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: diff --git a/.sonarcloud.properties b/.sonarcloud.properties new file mode 100644 index 0000000..8761d73 --- /dev/null +++ b/.sonarcloud.properties @@ -0,0 +1,2 @@ +sonar.sources=lib +sonar.tests=spec diff --git a/lib/configcat/config.rb b/lib/configcat/config.rb new file mode 100644 index 0000000..ddaa440 --- /dev/null +++ b/lib/configcat/config.rb @@ -0,0 +1,317 @@ +module ConfigCat + CONFIG_FILE_NAME = 'config_v6' + SERIALIZATION_FORMAT_VERSION = 'v2' + + # Config + PREFERENCES = 'p' + SEGMENTS = 's' + FEATURE_FLAGS = 'f' + + # Preferences + BASE_URL = 'u' + REDIRECT = 'r' + SALT = 's' + + # Segment + SEGMENT_NAME = 'n' # The first 4 characters of the Segment's name + SEGMENT_CONDITIONS = 'r' # The list of segment rule conditions (has a logical AND relation between the items). + + # Segment Condition (User Condition) + COMPARISON_ATTRIBUTE = 'a' # The attribute of the user object that should be used to evaluate this rule + COMPARATOR = 'c' + + # Feature flag (Evaluation Formula) + SETTING_TYPE = 't' # 0 = bool, 1 = string, 2 = int, 3 = double + PERCENTAGE_RULE_ATTRIBUTE = 'a' # Percentage rule evaluation hashes this attribute of the User object to calculate the buckets + TARGETING_RULES = 'r' # Targeting Rules (Logically connected by OR) + PERCENTAGE_OPTIONS = 'p' # Percentage Options without conditions + VALUE = 'v' + VARIATION_ID = 'i' + INLINE_SALT = 'inline_salt' + + # Targeting Rule (Evaluation Rule) + CONDITIONS = 'c' + SERVED_VALUE = 's' # Value and Variation ID + TARGETING_RULE_PERCENTAGE_OPTIONS = 'p' + + # Condition + USER_CONDITION = 'u' + SEGMENT_CONDITION = 's' # Segment targeting rule + PREREQUISITE_FLAG_CONDITION = 'p' # Prerequisite flag targeting rule + + # Segment Condition + SEGMENT_INDEX = 's' + SEGMENT_COMPARATOR = 'c' + INLINE_SEGMENT = 'inline_segment' + + # Prerequisite Flag Condition + PREREQUISITE_FLAG_KEY = 'f' + PREREQUISITE_COMPARATOR = 'c' + + # Percentage Option + PERCENTAGE = 'p' + + # Value + BOOL_VALUE = 'b' + STRING_VALUE = 's' + INT_VALUE = 'i' + DOUBLE_VALUE = 'd' + STRING_LIST_VALUE = 'l' + UNSUPPORTED_VALUE = 'unsupported_value' + + module Config + def self.is_type_mismatch(value, ruby_type) + is_float_int_mismatch = \ + (value.is_a?(Float) && ruby_type == Integer) || \ + (value.is_a?(Integer) && ruby_type == Float) + + is_bool_mismatch = value.is_a?(TrueClass) && ruby_type == FalseClass || \ + value.is_a?(FalseClass) && ruby_type == TrueClass + + if value.class != ruby_type && !is_float_int_mismatch && !is_bool_mismatch + return true + end + + return false + end + + def self.get_value(dictionary, setting_type) + value_descriptor = dictionary[VALUE] + if value_descriptor.nil? + raise 'Value is missing' + end + + expected_value_type, expected_ruby_type = SettingType.get_type_info(setting_type) + if expected_value_type.nil? + raise 'Unsupported setting type' + end + + value = value_descriptor[expected_value_type] + if value.nil? || is_type_mismatch(value, expected_ruby_type) + raise "Setting value is not of the expected type #{expected_ruby_type}" + end + + return value + end + + def self.get_value_type(dictionary) + value = dictionary[VALUE] + if !value.nil? + if !value[BOOL_VALUE].nil? + return TrueClass + end + if !value[STRING_VALUE].nil? + return String + end + if !value[INT_VALUE].nil? + return Integer + end + if !value[DOUBLE_VALUE].nil? + return Float + end + end + + return nil + end + + def self.fixup_config_salt_and_segments(config) + """ + Adds the inline salt and segment to the config. + When using flag overrides, the original salt and segment indexes may become invalid. Therefore, we copy the + object references to the locations where they are referenced and use these references instead of the indexes. + """ + salt = config.fetch(PREFERENCES, {}).fetch(SALT, '') + segments = config[SEGMENTS] || [] + settings = config[FEATURE_FLAGS] || {} + settings.each do |_, setting| + next unless setting.is_a?(Hash) + + # add salt + setting[INLINE_SALT] = salt + + # add segment to the segment conditions + targeting_rules = setting[TARGETING_RULES] || [] + targeting_rules.each do |targeting_rule| + conditions = targeting_rule[CONDITIONS] || [] + conditions.each do |condition| + segment_condition = condition[SEGMENT_CONDITION] + if segment_condition + segment_index = segment_condition[SEGMENT_INDEX] + segment = segments[segment_index] + segment_condition[INLINE_SEGMENT] = segment + end + end + end + end + end + end + + class SettingType + BOOL = 0 + STRING = 1 + INT = 2 + DOUBLE = 3 + + @@setting_type_mapping = { + SettingType::BOOL => [BOOL_VALUE, TrueClass], + SettingType::STRING => [STRING_VALUE, String], + SettingType::INT => [INT_VALUE, Integer], + SettingType::DOUBLE => [DOUBLE_VALUE, Float] + } + + def self.get_type_info(setting_type) + return @@setting_type_mapping[setting_type] || [nil, nil] + end + + def self.from_type(object_type) + if object_type == TrueClass || object_type == FalseClass + return BOOL + elsif object_type == String + return STRING + elsif object_type == Integer + return INT + elsif object_type == Float + return DOUBLE + end + + return nil + end + + def self.to_type(setting_type) + return get_type_info(setting_type)[1] + end + + def self.to_value_type(setting_type) + return get_type_info(setting_type)[0] + end + end + + module PrerequisiteComparator + EQUALS = 0 + NOT_EQUALS = 1 + end + + module SegmentComparator + IS_IN = 0 + IS_NOT_IN = 1 + end + + module Comparator + IS_ONE_OF = 0 + IS_NOT_ONE_OF = 1 + CONTAINS_ANY_OF = 2 + NOT_CONTAINS_ANY_OF = 3 + IS_ONE_OF_SEMVER = 4 + IS_NOT_ONE_OF_SEMVER = 5 + LESS_THAN_SEMVER = 6 + LESS_THAN_OR_EQUAL_SEMVER = 7 + GREATER_THAN_SEMVER = 8 + GREATER_THAN_OR_EQUAL_SEMVER = 9 + EQUALS_NUMBER = 10 + NOT_EQUALS_NUMBER = 11 + LESS_THAN_NUMBER = 12 + LESS_THAN_OR_EQUAL_NUMBER = 13 + GREATER_THAN_NUMBER = 14 + GREATER_THAN_OR_EQUAL_NUMBER = 15 + IS_ONE_OF_HASHED = 16 + IS_NOT_ONE_OF_HASHED = 17 + BEFORE_DATETIME = 18 + AFTER_DATETIME = 19 + EQUALS_HASHED = 20 + NOT_EQUALS_HASHED = 21 + STARTS_WITH_ANY_OF_HASHED = 22 + NOT_STARTS_WITH_ANY_OF_HASHED = 23 + ENDS_WITH_ANY_OF_HASHED = 24 + NOT_ENDS_WITH_ANY_OF_HASHED = 25 + ARRAY_CONTAINS_ANY_OF_HASHED = 26 + ARRAY_NOT_CONTAINS_ANY_OF_HASHED = 27 + EQUALS = 28 + NOT_EQUALS = 29 + STARTS_WITH_ANY_OF = 30 + NOT_STARTS_WITH_ANY_OF = 31 + ENDS_WITH_ANY_OF = 32 + NOT_ENDS_WITH_ANY_OF = 33 + ARRAY_CONTAINS_ANY_OF = 34 + ARRAY_NOT_CONTAINS_ANY_OF = 35 + end + + COMPARATOR_TEXTS = [ + 'IS ONE OF', # IS_ONE_OF + 'IS NOT ONE OF', # IS_NOT_ONE_OF + 'CONTAINS ANY OF', # CONTAINS_ANY_OF + 'NOT CONTAINS ANY OF', # NOT_CONTAINS_ANY_OF + 'IS ONE OF', # IS_ONE_OF_SEMVER + 'IS NOT ONE OF', # IS_NOT_ONE_OF_SEMVER + '<', # LESS_THAN_SEMVER + '<=', # LESS_THAN_OR_EQUAL_SEMVER + '>', # GREATER_THAN_SEMVER + '>=', # GREATER_THAN_OR_EQUAL_SEMVER + '=', # EQUALS_NUMBER + '!=', # NOT_EQUALS_NUMBER + '<', # LESS_THAN_NUMBER + '<=', # LESS_THAN_OR_EQUAL_NUMBER + '>', # GREATER_THAN_NUMBER + '>=', # GREATER_THAN_OR_EQUAL_NUMBER + 'IS ONE OF', # IS_ONE_OF_HASHED + 'IS NOT ONE OF', # IS_NOT_ONE_OF_HASHED + 'BEFORE', # BEFORE_DATETIME + 'AFTER', # AFTER_DATETIME + 'EQUALS', # EQUALS_HASHED + 'NOT EQUALS', # NOT_EQUALS_HASHED + 'STARTS WITH ANY OF', # STARTS_WITH_ANY_OF_HASHED + 'NOT STARTS WITH ANY OF', # NOT_STARTS_WITH_ANY_OF_HASHED + 'ENDS WITH ANY OF', # ENDS_WITH_ANY_OF_HASHED + 'NOT ENDS WITH ANY OF', # NOT_ENDS_WITH_ANY_OF_HASHED + 'ARRAY CONTAINS ANY OF', # ARRAY_CONTAINS_ANY_OF_HASHED + 'ARRAY NOT CONTAINS ANY OF', # ARRAY_NOT_CONTAINS_ANY_OF_HASHED + 'EQUALS', # EQUALS + 'NOT EQUALS', # NOT_EQUALS + 'STARTS WITH ANY OF', # STARTS_WITH_ANY_OF + 'NOT STARTS WITH ANY OF', # NOT_STARTS_WITH_ANY_OF + 'ENDS WITH ANY OF', # ENDS_WITH_ANY_OF + 'NOT ENDS WITH ANY OF', # NOT_ENDS_WITH_ANY_OF + 'ARRAY CONTAINS ANY OF', # ARRAY_CONTAINS_ANY_OF + 'ARRAY NOT CONTAINS ANY OF' # ARRAY_NOT_CONTAINS_ANY_OF + ] + + COMPARISON_VALUES = [ + STRING_LIST_VALUE, # IS_ONE_OF + STRING_LIST_VALUE, # IS_NOT_ONE_OF + STRING_LIST_VALUE, # CONTAINS_ANY_OF + STRING_LIST_VALUE, # NOT_CONTAINS_ANY_OF + STRING_LIST_VALUE, # IS_ONE_OF_SEMVER + STRING_LIST_VALUE, # IS_NOT_ONE_OF_SEMVER + STRING_VALUE, # LESS_THAN_SEMVER + STRING_VALUE, # LESS_THAN_OR_EQUAL_SEMVER + STRING_VALUE, # GREATER_THAN_SEMVER + STRING_VALUE, # GREATER_THAN_OR_EQUAL_SEMVER + DOUBLE_VALUE, # EQUALS_NUMBER + DOUBLE_VALUE, # NOT_EQUALS_NUMBER + DOUBLE_VALUE, # LESS_THAN_NUMBER + DOUBLE_VALUE, # LESS_THAN_OR_EQUAL_NUMBER + DOUBLE_VALUE, # GREATER_THAN_NUMBER + DOUBLE_VALUE, # GREATER_THAN_OR_EQUAL_NUMBER + STRING_LIST_VALUE, # IS_ONE_OF_HASHED + STRING_LIST_VALUE, # IS_NOT_ONE_OF_HASHED + DOUBLE_VALUE, # BEFORE_DATETIME + DOUBLE_VALUE, # AFTER_DATETIME + STRING_VALUE, # EQUALS_HASHED + STRING_VALUE, # NOT_EQUALS_HASHED + STRING_LIST_VALUE, # STARTS_WITH_ANY_OF_HASHED + STRING_LIST_VALUE, # NOT_STARTS_WITH_ANY_OF_HASHED + STRING_LIST_VALUE, # ENDS_WITH_ANY_OF_HASHED + STRING_LIST_VALUE, # NOT_ENDS_WITH_ANY_OF_HASHED + STRING_LIST_VALUE, # ARRAY_CONTAINS_ANY_OF_HASHED + STRING_LIST_VALUE, # ARRAY_NOT_CONTAINS_ANY_OF_HASHED + STRING_VALUE, # EQUALS + STRING_VALUE, # NOT_EQUALS + STRING_LIST_VALUE, # STARTS_WITH_ANY_OF + STRING_LIST_VALUE, # NOT_STARTS_WITH_ANY_OF + STRING_LIST_VALUE, # ENDS_WITH_ANY_OF + STRING_LIST_VALUE, # NOT_ENDS_WITH_ANY_OF + STRING_LIST_VALUE, # ARRAY_CONTAINS_ANY_OF + STRING_LIST_VALUE # ARRAY_NOT_CONTAINS_ANY_OF + ] + SEGMENT_COMPARATOR_TEXTS = ['IS IN SEGMENT', 'IS NOT IN SEGMENT'] + PREREQUISITE_COMPARATOR_TEXTS = ['EQUALS', 'DOES NOT EQUAL'] +end diff --git a/lib/configcat/configcatclient.rb b/lib/configcat/configcatclient.rb index 0bca562..94f82fe 100644 --- a/lib/configcat/configcatclient.rb +++ b/lib/configcat/configcatclient.rb @@ -60,15 +60,26 @@ def self.close_all raise ConfigCatClientException, "SDK Key is required." end - @_sdk_key = sdk_key - @_default_user = options.default_user - @_rollout_evaluator = RolloutEvaluator.new(@log) if options.flag_overrides @_override_data_source = options.flag_overrides.create_data_source(@log) else @_override_data_source = nil end + # In case of local only flag overrides mode, we accept any SDK Key format. + if @_override_data_source.nil? || @_override_data_source.get_behaviour() != OverrideBehaviour::LOCAL_ONLY + is_valid_sdk_key = /^.{22}\/.{22}$/.match?(sdk_key) || + /^configcat-sdk-1\/.{22}\/.{22}$/.match?(sdk_key) || + (options.base_url && /^configcat-proxy\/.+$/.match?(sdk_key)) + unless is_valid_sdk_key + raise ConfigCatClientException, "SDK Key `#{sdk_key}` is invalid." + end + end + + @_sdk_key = sdk_key + @_default_user = options.default_user + @_rollout_evaluator = RolloutEvaluator.new(@log) + config_cache = options.config_cache.nil? ? NullConfigCache.new : options.config_cache if @_override_data_source && @_override_data_source.get_behaviour() == OverrideBehaviour::LOCAL_ONLY @@ -104,14 +115,14 @@ def self.close_all # :param user [User] the user object to identify the caller. # :return the value. def get_value(key, default_value, user = nil) - settings, fetch_time = _get_settings() - if settings.nil? + config, fetch_time = _get_config() + if config.nil? || config[FEATURE_FLAGS].nil? message = "Config JSON is not present when evaluating setting '#{key}'. Returning the `default_value` parameter that you specified in your application: '#{default_value}'." @log.error(1000, message) @hooks.invoke_on_flag_evaluated(EvaluationDetails.from_error(key, default_value, error: message)) return default_value end - details = _evaluate(key, user, default_value, nil, settings, fetch_time) + details = _evaluate(key, user, default_value, nil, config, fetch_time) return details.value end @@ -122,15 +133,15 @@ def get_value(key, default_value, user = nil) # :param user [User] the user object to identify the caller. # :return [EvaluationDetails] the evaluation details. def get_value_details(key, default_value, user = nil) - settings, fetch_time = _get_settings() - if settings.nil? + config, fetch_time = _get_config() + if config.nil? || config[FEATURE_FLAGS].nil? message = "Config JSON is not present when evaluating setting '#{key}'. Returning the `default_value` parameter that you specified in your application: '#{default_value}'." @log.error(1000, message) details = EvaluationDetails.from_error(key, default_value, error: message) @hooks.invoke_on_flag_evaluated(details) return details end - details = _evaluate(key, user, default_value, nil, settings, fetch_time) + details = _evaluate(key, user, default_value, nil, config, fetch_time) return details end @@ -138,11 +149,12 @@ def get_value_details(key, default_value, user = nil) # # :return list of keys. def get_all_keys - settings, _ = _get_settings() - if settings === nil + config, _ = _get_config() + if config.nil? @log.error(1000, "Config JSON is not present. Returning empty list.") return [] end + settings = config.fetch(FEATURE_FLAGS, {}) return settings.keys end @@ -151,30 +163,47 @@ def get_all_keys # :param variation_id [String] variation ID # :return key and value def get_key_and_value(variation_id) - settings, _ = _get_settings() - if settings === nil + config, _ = _get_config() + if config.nil? @log.error(1000, "Config JSON is not present. Returning nil.") return nil end - for key, value in settings - if variation_id == value.fetch(VARIATION_ID, nil) - return KeyValue.new(key, value[VALUE]) - end + settings = config.fetch(FEATURE_FLAGS, {}) + begin + settings.each do |key, value| + setting_type = value.fetch(SETTING_TYPE, nil) + if variation_id == value.fetch(VARIATION_ID, nil) + return KeyValue.new(key, Config.get_value(value, setting_type)) + end - rollout_rules = value.fetch(ROLLOUT_RULES, []) - for rollout_rule in rollout_rules - if variation_id == rollout_rule.fetch(VARIATION_ID, nil) - return KeyValue.new(key, rollout_rule[VALUE]) + targeting_rules = value.fetch(TARGETING_RULES, []) + targeting_rules.each do |targeting_rule| + served_value = targeting_rule.fetch(SERVED_VALUE, nil) + if !served_value.nil? + if variation_id == served_value.fetch(VARIATION_ID, nil) + return KeyValue.new(key, Config.get_value(served_value, setting_type)) + end + else + percentage_options = targeting_rule.fetch(PERCENTAGE_OPTIONS, []) + percentage_options.each do |percentage_option| + if variation_id == percentage_option.fetch(VARIATION_ID, nil) + return KeyValue.new(key, Config.get_value(percentage_option, setting_type)) + end + end + end end - end - rollout_percentage_items = value.fetch(ROLLOUT_PERCENTAGE_ITEMS, []) - for rollout_percentage_item in rollout_percentage_items - if variation_id == rollout_percentage_item.fetch(VARIATION_ID, nil) - return KeyValue.new(key, rollout_percentage_item[VALUE]) + percentage_options = value.fetch(PERCENTAGE_OPTIONS, []) + percentage_options.each do |percentage_option| + if variation_id == percentage_option.fetch(VARIATION_ID, nil) + return KeyValue.new(key, Config.get_value(percentage_option, setting_type)) + end end end + rescue => e + @log.error("Error occurred in the `#{self.class.name}` method. Returning nil.", event_id: 1002) + return nil end @log.error(2011, "Could not find the setting for the specified variation ID: '#{variation_id}'.") @@ -185,12 +214,13 @@ def get_key_and_value(variation_id) # :param user [User] the user object to identify the caller. # :return dictionary of values def get_all_values(user = nil) - settings, _ = _get_settings() - if settings === nil + config, _ = _get_config() + if config.nil? @log.error(1000, "Config JSON is not present. Returning empty dictionary.") return {} end + settings = config.fetch(FEATURE_FLAGS, {}) all_values = {} for key in settings.keys value = get_value(key, nil, user) @@ -206,15 +236,16 @@ def get_all_values(user = nil) # :param user [User] the user object to identify the caller. # :return list of all evaluation details def get_all_value_details(user = nil) - settings, fetch_time = _get_settings() - if settings.nil? + config, fetch_time = _get_config() + if config.nil? @log.error(1000, "Config JSON is not present. Returning empty list.") return [] end details_result = [] + settings = config.fetch(FEATURE_FLAGS, {}) for key in settings.keys - details = _evaluate(key, user, nil, nil, settings, fetch_time) + details = _evaluate(key, user, nil, nil, config, fetch_time) details_result.push(details) end @@ -227,8 +258,8 @@ def get_all_value_details(user = nil) def force_refresh return @_config_service.refresh if @_config_service - return RefreshResult(false, - "The SDK uses the LocalOnly flag override behavior which prevents making HTTP requests.") + return RefreshResult.new(false, + "The SDK uses the LocalOnly flag override behavior which prevents making HTTP requests.") end # Sets the default user. @@ -278,40 +309,57 @@ def _close_resources @hooks.clear end - def _get_settings + def _get_config if !@_override_data_source.nil? behaviour = @_override_data_source.get_behaviour() if behaviour == OverrideBehaviour::LOCAL_ONLY return @_override_data_source.get_overrides(), Utils::DISTANT_PAST elsif behaviour == OverrideBehaviour::REMOTE_OVER_LOCAL - remote_settings, fetch_time = @_config_service.get_settings() - local_settings = @_override_data_source.get_overrides() - remote_settings ||= {} - local_settings ||= {} - result = local_settings.clone() - result.update(remote_settings) + remote_config, fetch_time = @_config_service.get_config() + local_config = @_override_data_source.get_overrides() + remote_config ||= { FEATURE_FLAGS => {} } + local_config ||= { FEATURE_FLAGS => {} } + result = local_config.clone() + result[FEATURE_FLAGS].update(remote_config[FEATURE_FLAGS]) return result, fetch_time elsif behaviour == OverrideBehaviour::LOCAL_OVER_REMOTE - remote_settings, fetch_time = @_config_service.get_settings() - local_settings = @_override_data_source.get_overrides() - remote_settings ||= {} - local_settings ||= {} - result = remote_settings.clone() - result.update(local_settings) + remote_config, fetch_time = @_config_service.get_config() + local_config = @_override_data_source.get_overrides() + remote_config ||= { FEATURE_FLAGS => {} } + local_config ||= { FEATURE_FLAGS => {} } + result = remote_config.clone() + result[FEATURE_FLAGS].update(local_config[FEATURE_FLAGS]) return result, fetch_time end end - return @_config_service.get_settings() + return @_config_service.get_config() + end + + def _check_type_mismatch(value, default_value) + if !default_value.nil? && Config.is_type_mismatch(value, default_value.class) + @log.warn(4002, "The type of a setting does not match the type of the specified default value (#{default_value}). " \ + "Setting's type was #{value.class} but the default value's type was #{default_value.class}. " \ + "Please make sure that using a default value not matching the setting's type was intended.") + end end - def _evaluate(key, user, default_value, default_variation_id, settings, fetch_time) - user = user || @_default_user + def _evaluate(key, user, default_value, default_variation_id, config, fetch_time) + user ||= @_default_user + + # Skip building the evaluation log if it won't be logged. + log_builder = EvaluationLogBuilder.new if @log.enabled_for?(Logger::INFO) + value, variation_id, rule, percentage_rule, error = @_rollout_evaluator.evaluate( key: key, user: user, default_value: default_value, default_variation_id: default_variation_id, - settings: settings) + config: config, + log_builder: log_builder) + + _check_type_mismatch(value, default_value) + + @log.info(5000, log_builder.to_s) if log_builder details = EvaluationDetails.new(key: key, value: value, @@ -320,8 +368,8 @@ def _evaluate(key, user, default_value, default_variation_id, settings, fetch_ti user: user, is_default_value: error.nil? || error.empty? ? false : true, error: error, - matched_evaluation_rule: rule, - matched_evaluation_percentage_rule: percentage_rule) + matched_targeting_rule: rule, + matched_percentage_option: percentage_rule) @hooks.invoke_on_flag_evaluated(details) return details end diff --git a/lib/configcat/configcatlogger.rb b/lib/configcat/configcatlogger.rb index b30c1d5..586e05d 100644 --- a/lib/configcat/configcatlogger.rb +++ b/lib/configcat/configcatlogger.rb @@ -4,6 +4,10 @@ def initialize(hooks) @hooks = hooks end + def enabled_for?(log_level) + ConfigCat.logger.level <= log_level + end + def debug(message) ConfigCat.logger.debug("[0] " + message) end diff --git a/lib/configcat/configentry.rb b/lib/configcat/configentry.rb index 7fa6815..f1b0a23 100644 --- a/lib/configcat/configentry.rb +++ b/lib/configcat/configentry.rb @@ -41,6 +41,7 @@ def self.create_from_string(string) begin config_json = string[etag_index + 1..-1] config = JSON.parse(config_json) + Config.fixup_config_salt_and_segments(config) rescue => e raise "Invalid config JSON: #{config_json}. #{e.message}" end diff --git a/lib/configcat/configfetcher.rb b/lib/configcat/configfetcher.rb index 15cd4e0..f5a971e 100644 --- a/lib/configcat/configfetcher.rb +++ b/lib/configcat/configfetcher.rb @@ -1,7 +1,7 @@ require 'configcat/interfaces' require 'configcat/version' require 'configcat/datagovernance' -require 'configcat/constants' +require 'configcat/config' require 'configcat/configentry' require 'net/http' require 'uri' @@ -20,8 +20,8 @@ class RedirectMode end class Status - FETCHED = 0, - NOT_MODIFIED = 1, + FETCHED = 0 + NOT_MODIFIED = 1 FAILURE = 2 end @@ -179,6 +179,7 @@ def _fetch(etag) response_etag = "" end config = JSON.parse(response.body) + Config.fixup_config_salt_and_segments(config) return FetchResponse.success(ConfigEntry.new(config, response_etag, response.body, Utils.get_utc_now_seconds_since_epoch)) when Net::HTTPNotModified return FetchResponse.not_modified @@ -198,7 +199,9 @@ def _fetch(etag) @log.error(1102, error) return FetchResponse.failure(error, true) rescue Exception => e - error = "Unexpected error occurred while trying to fetch config JSON: #{e}" + error = "Unexpected error occurred while trying to fetch config JSON. It is most likely due to a local network " \ + "issue. Please make sure your application can reach the ConfigCat CDN servers (or your proxy server) " \ + "over HTTP. #{e}" @log.error(1103, error) return FetchResponse.failure(error, true) end diff --git a/lib/configcat/configservice.rb b/lib/configcat/configservice.rb index 66a602b..9000f40 100644 --- a/lib/configcat/configservice.rb +++ b/lib/configcat/configservice.rb @@ -30,13 +30,12 @@ def initialize(sdk_key, polling_mode, hooks, config_fetcher, log, config_cache, end end - def get_settings + def get_config if @polling_mode.is_a?(LazyLoadingMode) entry, _ = fetch_if_older(Utils.get_utc_now_seconds_since_epoch - @polling_mode.cache_refresh_interval_seconds) return !entry.empty? ? - [entry.config.fetch(FEATURE_FLAGS, {}), entry.fetch_time] : + [entry.config, entry.fetch_time] : [nil, Utils::DISTANT_PAST] - elsif @polling_mode.is_a?(AutoPollingMode) && !@initialized.set? elapsed_time = Utils.get_utc_now_seconds_since_epoch - @start_time # Elapsed time in seconds if elapsed_time < @polling_mode.max_init_wait_time_seconds @@ -46,20 +45,27 @@ def get_settings if !@initialized.set? set_initialized return !@cached_entry.empty? ? - [@cached_entry.config.fetch(FEATURE_FLAGS, {}), @cached_entry.fetch_time] : + [@cached_entry.config, @cached_entry.fetch_time] : [nil, Utils::DISTANT_PAST] end end end - entry, _ = fetch_if_older(Utils::DISTANT_PAST, prefer_cache: true) + # If we are initialized, we prefer the cached results + entry, _ = fetch_if_older(Utils::DISTANT_PAST, prefer_cache: @initialized.set?) return !entry.empty? ? - [entry.config.fetch(FEATURE_FLAGS, {}), entry.fetch_time] : + [entry.config, entry.fetch_time] : [nil, Utils::DISTANT_PAST] end # :return [RefreshResult] def refresh + if offline? + offline_warning = "Client is in offline mode, it cannot initiate HTTP calls." + @log.warn(3200, offline_warning) + return RefreshResult.new(success = false, error = offline_warning) + end + _, error = fetch_if_older(Utils::DISTANT_FUTURE) return RefreshResult.new(success = error.nil?, error = error) end @@ -74,7 +80,7 @@ def set_online if @polling_mode.is_a?(AutoPollingMode) start_poll end - @log.info(5200, 'Switched to ONLINE mode.') + @log.info(5200, "Switched to ONLINE mode.") end end @@ -90,7 +96,7 @@ def set_offline @thread.join end - @log.info(5200, 'Switched to OFFLINE mode.') + @log.info(5200, "Switched to OFFLINE mode.") end end @@ -111,35 +117,25 @@ def self.get_cache_key(sdk_key) end # :return [ConfigEntry, String] Returns the ConfigEntry object and error message in case of any error. - def fetch_if_older(time, prefer_cache: false) + def fetch_if_older(threshold, prefer_cache: false) # Sync up with the cache and use it when it's not expired. @lock.synchronize do - if @cached_entry.empty? || @cached_entry.fetch_time > time - entry = read_cache - if !entry.empty? && entry.etag != @cached_entry.etag - @cached_entry = entry - @hooks.invoke_on_config_changed(entry.config[FEATURE_FLAGS]) - end - - # Cache isn't expired - if @cached_entry.fetch_time > time - set_initialized - return @cached_entry, nil - end + # Sync up with the cache and use it when it's not expired. + from_cache = read_cache + if !from_cache.empty? && from_cache.etag != @cached_entry.etag + @cached_entry = from_cache + @hooks.invoke_on_config_changed(from_cache.config[FEATURE_FLAGS]) end - # Use cache anyway (get calls on auto & manual poll must not initiate fetch). - # The initialized check ensures that we subscribe for the ongoing fetch during the - # max init wait time window in case of auto poll. - if prefer_cache && @initialized.set? + # Cache isn't expired + if @cached_entry.fetch_time > threshold + set_initialized return @cached_entry, nil end - # If we are in offline mode we are not allowed to initiate fetch. - if @is_offline - offline_warning = "Client is in offline mode, it cannot initiate HTTP calls." - @log.warn(3200, offline_warning) - return @cached_entry, offline_warning + # If we are in offline mode or the caller prefers cached values, do not initiate fetch. + if @is_offline || prefer_cache + return @cached_entry, nil end end diff --git a/lib/configcat/constants.rb b/lib/configcat/constants.rb deleted file mode 100644 index cd67ac0..0000000 --- a/lib/configcat/constants.rb +++ /dev/null @@ -1,18 +0,0 @@ -module ConfigCat - CONFIG_FILE_NAME = "config_v5" - SERIALIZATION_FORMAT_VERSION = "v2" - - PREFERENCES = "p" - BASE_URL = "u" - REDIRECT = "r" - - FEATURE_FLAGS = "f" - VALUE = "v" - COMPARATOR = "t" - COMPARISON_ATTRIBUTE = "a" - COMPARISON_VALUE = "c" - ROLLOUT_PERCENTAGE_ITEMS = "p" - PERCENTAGE = "p" - ROLLOUT_RULES = "r" - VARIATION_ID = "i" -end diff --git a/lib/configcat/evaluationcontext.rb b/lib/configcat/evaluationcontext.rb new file mode 100644 index 0000000..3247dbf --- /dev/null +++ b/lib/configcat/evaluationcontext.rb @@ -0,0 +1,14 @@ +module ConfigCat + class EvaluationContext + attr_accessor :key, :setting_type, :user, :visited_keys, :is_missing_user_object_logged, :is_missing_user_object_attribute_logged + + def initialize(key, setting_type, user, visited_keys = nil, is_missing_user_object_logged = false, is_missing_user_object_attribute_logged = false) + @key = key + @setting_type = setting_type + @user = user + @visited_keys = visited_keys || [] + @is_missing_user_object_logged = is_missing_user_object_logged + @is_missing_user_object_attribute_logged = is_missing_user_object_attribute_logged + end + end +end diff --git a/lib/configcat/evaluationdetails.rb b/lib/configcat/evaluationdetails.rb index 3232592..e5b70aa 100644 --- a/lib/configcat/evaluationdetails.rb +++ b/lib/configcat/evaluationdetails.rb @@ -1,19 +1,37 @@ module ConfigCat class EvaluationDetails attr_reader :key, :value, :variation_id, :fetch_time, :user, :is_default_value, :error, - :matched_evaluation_rule, :matched_evaluation_percentage_rule + :matched_targeting_rule, :matched_percentage_option def initialize(key:, value:, variation_id: nil, fetch_time: nil, user: nil, is_default_value: false, error: nil, - matched_evaluation_rule: nil, matched_evaluation_percentage_rule: nil) + matched_targeting_rule: nil, matched_percentage_option: nil) + # Key of the feature flag or setting. @key = key + + # Evaluated value of the feature flag or setting. @value = value + + # Variation ID of the feature flag or setting (if available). @variation_id = variation_id + + # Time of last successful config download. @fetch_time = fetch_time + + # The User Object used for the evaluation (if available). @user = user + + # Indicates whether the default value passed to the setting evaluation methods like ConfigCatClient.get_value, + # ConfigCatClient.get_value_details, etc. is used as the result of the evaluation. @is_default_value = is_default_value + + # Error message in case evaluation failed. @error = error - @matched_evaluation_rule = matched_evaluation_rule - @matched_evaluation_percentage_rule = matched_evaluation_percentage_rule + + # The targeting rule (if any) that matched during the evaluation and was used to return the evaluated value. + @matched_targeting_rule = matched_targeting_rule + + # The percentage option (if any) that was used to select the evaluated value. + @matched_percentage_option = matched_percentage_option end def self.from_error(key, value, error:, variation_id: nil) diff --git a/lib/configcat/evaluationlogbuilder.rb b/lib/configcat/evaluationlogbuilder.rb new file mode 100644 index 0000000..dbab099 --- /dev/null +++ b/lib/configcat/evaluationlogbuilder.rb @@ -0,0 +1,81 @@ +module ConfigCat + class EvaluationLogBuilder + def initialize + @indent_level = 0 + @text = '' + end + + def self.trunc_comparison_value_if_needed(comparator, comparison_value) + if [ + Comparator::IS_ONE_OF_HASHED, + Comparator::IS_NOT_ONE_OF_HASHED, + Comparator::EQUALS_HASHED, + Comparator::NOT_EQUALS_HASHED, + Comparator::STARTS_WITH_ANY_OF_HASHED, + Comparator::NOT_STARTS_WITH_ANY_OF_HASHED, + Comparator::ENDS_WITH_ANY_OF_HASHED, + Comparator::NOT_ENDS_WITH_ANY_OF_HASHED, + Comparator::ARRAY_CONTAINS_ANY_OF_HASHED, + Comparator::ARRAY_NOT_CONTAINS_ANY_OF_HASHED + ].include?(comparator) + if comparison_value.is_a?(Array) + length = comparison_value.length + if length > 1 + return "[<#{length} hashed values>]" + end + return "[<#{length} hashed value>]" + end + + return "''" + end + + if comparison_value.is_a?(Array) + length_limit = 10 + length = comparison_value.length + if length > length_limit + remaining = length - length_limit + more_text = remaining == 1 ? "<1 more value>" : "<#{remaining} more values>" + + formatted_strings = comparison_value.first(length_limit).map { |str| "'#{str}'" }.join(", ") + return "[#{formatted_strings}, ... #{more_text}]" + end + + # replace '"' with "'" in the string representation of the array + formatted_strings = comparison_value.map { |str| "'#{str}'" }.join(", ") + return "[#{formatted_strings}]" + end + + if [Comparator::BEFORE_DATETIME, Comparator::AFTER_DATETIME].include?(comparator) + time = Utils.get_date_time(comparison_value) + return "'#{comparison_value}' (#{time.strftime('%Y-%m-%dT%H:%M:%S.%L')}Z UTC)" + end + + "'#{comparison_value.to_s}'" + end + + def increase_indent + @indent_level += 1 + self + end + + def decrease_indent + @indent_level = [@indent_level - 1, 0].max + self + end + + def append(text) + @text += text + self + end + + def new_line(text = nil) + @text += "\n" + ' ' * @indent_level + @text += text if text + self + end + + def to_s + @text + end + end +end diff --git a/lib/configcat/localdictionarydatasource.rb b/lib/configcat/localdictionarydatasource.rb index 9c55eec..5231e78 100644 --- a/lib/configcat/localdictionarydatasource.rb +++ b/lib/configcat/localdictionarydatasource.rb @@ -1,5 +1,5 @@ require 'configcat/overridedatasource' -require 'configcat/constants' +require 'configcat/config' module ConfigCat @@ -17,14 +17,30 @@ def create_data_source(log) class LocalDictionaryDataSource < OverrideDataSource def initialize(source, override_behaviour) super(override_behaviour) - @_settings = {} + @_config = {} source.each do |key, value| - @_settings[key] = { VALUE => value } + value_type = case value + when TrueClass, FalseClass + BOOL_VALUE + when String + STRING_VALUE + when Integer + INT_VALUE + when Float + DOUBLE_VALUE + else + UNSUPPORTED_VALUE + end + + @_config[FEATURE_FLAGS] ||= {} + @_config[FEATURE_FLAGS][key] = { VALUE => { value_type => value } } + setting_type = SettingType.from_type(value.class) + @_config[FEATURE_FLAGS][key][SETTING_TYPE] = setting_type.to_i unless setting_type.nil? end end def get_overrides - return @_settings + return @_config end end end diff --git a/lib/configcat/localfiledatasource.rb b/lib/configcat/localfiledatasource.rb index 6006d45..f085fa5 100644 --- a/lib/configcat/localfiledatasource.rb +++ b/lib/configcat/localfiledatasource.rb @@ -1,5 +1,5 @@ require 'configcat/overridedatasource' -require 'configcat/constants' +require 'configcat/config' module ConfigCat @@ -18,17 +18,17 @@ class LocalFileDataSource < OverrideDataSource def initialize(file_path, override_behaviour, log) super(override_behaviour) @log = log - if !File.exists?(file_path) + if !File.exist?(file_path) @log.error(1300, "Cannot find the local config file '#{file_path}'. This is a path that your application provided to the ConfigCat SDK by passing it to the `LocalFileFlagOverrides.new()` method. Read more: https://configcat.com/docs/sdk-reference/ruby/#json-file") end @_file_path = file_path - @_settings = nil + @_config = nil @_cached_file_stamp = 0 end def get_overrides reload_file_content() - return @_settings + return @_config end private @@ -41,13 +41,29 @@ def reload_file_content file = File.read(@_file_path) data = JSON.parse(file) if data.key?("flags") - @_settings = {} + @_config = { FEATURE_FLAGS => {} } source = data["flags"] source.each do |key, value| - @_settings[key] = { VALUE => value } + value_type = case value + when true, false + BOOL_VALUE + when String + STRING_VALUE + when Integer + INT_VALUE + when Float + DOUBLE_VALUE + else + UNSUPPORTED_VALUE + end + + @_config[FEATURE_FLAGS][key] = { VALUE => { value_type => value } } + setting_type = SettingType.from_type(value.class) + @_config[FEATURE_FLAGS][key][SETTING_TYPE] = setting_type.to_i unless setting_type.nil? end else - @_settings = data[FEATURE_FLAGS] + Config.fixup_config_salt_and_segments(data) + @_config = data end end rescue JSON::ParserError => e diff --git a/lib/configcat/rolloutevaluator.rb b/lib/configcat/rolloutevaluator.rb index 35c91c6..266f3d1 100644 --- a/lib/configcat/rolloutevaluator.rb +++ b/lib/configcat/rolloutevaluator.rb @@ -1,20 +1,26 @@ require 'configcat/user' -require 'configcat/constants' +require 'configcat/config' +require 'configcat/evaluationcontext' +require 'configcat/evaluationlogbuilder' require 'digest' require 'semantic' + module ConfigCat class RolloutEvaluator - COMPARATOR_TEXTS = ["IS ONE OF", "IS NOT ONE OF", "CONTAINS", "DOES NOT CONTAIN", "IS ONE OF (SemVer)", "IS NOT ONE OF (SemVer)", "< (SemVer)", "<= (SemVer)", "> (SemVer)", ">= (SemVer)", "= (Number)", "<> (Number)", "< (Number)", "<= (Number)", "> (Number)", ">= (Number)"] - def initialize(log) @log = log end - # :returns value, variation_id. matched_evaluation_rule, matched_evaluation_percentage_rule, error - def evaluate(key:, user:, default_value:, default_variation_id:, settings:) + # :returns value, variation_id. matched_targeting_rule, matched_percentage_option, error + def evaluate(key:, user:, default_value:, default_variation_id:, config:, log_builder:, visited_keys: nil) + visited_keys ||= [] + is_root_flag_evaluation = visited_keys.empty? + + settings = config[FEATURE_FLAGS] || {} setting_descriptor = settings[key] - if setting_descriptor === nil + + if setting_descriptor.nil? error = "Failed to evaluate setting '#{key}' (the key was not found in config JSON). " \ "Returning the `default_value` parameter that you specified in your application: '#{default_value}'. " \ "Available keys: [#{settings.keys.map { |s| "'#{s}'" }.join(", ")}]." @@ -22,176 +28,733 @@ def evaluate(key:, user:, default_value:, default_variation_id:, settings:) return default_value, default_variation_id, nil, nil, error end - rollout_rules = setting_descriptor.fetch(ROLLOUT_RULES, []) - rollout_percentage_items = setting_descriptor.fetch(ROLLOUT_PERCENTAGE_ITEMS, []) + setting_type = setting_descriptor[SETTING_TYPE] + salt = setting_descriptor[INLINE_SALT] || '' + targeting_rules = setting_descriptor[TARGETING_RULES] || [] + percentage_rule_attribute = setting_descriptor[PERCENTAGE_RULE_ATTRIBUTE] - user_has_invalid_type = !user.equal?(nil) && !user.class.equal?(User) + context = EvaluationContext.new(key, setting_type, user, visited_keys) + user_has_invalid_type = context.user && !context.user.is_a?(User) if user_has_invalid_type - @log.warn(4001, "Cannot evaluate targeting rules and % options for setting '#{key}' (User Object is not an instance of User type).") - user = nil + @log.warn(4001, "Cannot evaluate targeting rules and % options for setting '#{key}' " \ + "(User Object is not an instance of User type). " \ + "You should pass a User Object to the evaluation methods like `get_value()` " \ + "in order to make targeting work properly. " \ + "Read more: https://configcat.com/docs/advanced/user-object/") + # We set the user to nil and won't log further missing user object warnings + context.user = nil + context.is_missing_user_object_logged = true end - if user === nil - if !user_has_invalid_type && (rollout_rules.size > 0 || rollout_percentage_items.size > 0) - @log.warn(3001, "Cannot evaluate targeting rules and % options for setting '#{key}' (User Object is missing). " \ - "You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. " \ - "Read more: https://configcat.com/docs/advanced/user-object/") - end - return_value = setting_descriptor.fetch(VALUE, default_value) - return_variation_id = setting_descriptor.fetch(VARIATION_ID, default_variation_id) - @log.info(5000, "Returning [#{return_value}]") - return return_value, return_variation_id, nil, nil, nil - end - - log_entries = ["Evaluating get_value('%s')." % key, "User object:\n%s" % user.to_s] begin - # Evaluate targeting rules - for rollout_rule in rollout_rules - comparison_attribute = rollout_rule.fetch(COMPARISON_ATTRIBUTE) - comparison_value = rollout_rule.fetch(COMPARISON_VALUE, nil) - comparator = rollout_rule.fetch(COMPARATOR, nil) - - user_value = user.get_attribute(comparison_attribute) - if user_value === nil || !user_value - log_entries.push(format_no_match_rule(comparison_attribute, user_value, comparator, comparison_value)) - next - end + if log_builder && is_root_flag_evaluation + log_builder.append("Evaluating '#{key}'") + log_builder.append(" for User '#{context.user}'") if context.user + log_builder.increase_indent + end + + # Evaluate targeting rules (logically connected by OR) + if log_builder && targeting_rules.any? + log_builder.new_line("Evaluating targeting rules and applying the first match if any:") + end + targeting_rules.each do |targeting_rule| + conditions = targeting_rule[CONDITIONS] || [] - value = rollout_rule.fetch(VALUE, nil) - variation_id = rollout_rule.fetch(VARIATION_ID, default_variation_id) + if conditions.any? + served_value = targeting_rule[SERVED_VALUE] + value = Config.get_value(served_value, setting_type) if served_value - # IS ONE OF - if comparator == 0 - if comparison_value.to_s.split(",").map { |x| x.strip() }.include?(user_value.to_s) - log_entries.push(format_match_rule(comparison_attribute, user_value, comparator, comparison_value, value)) - return value, variation_id, rollout_rule, nil, nil - end - # IS NOT ONE OF - elsif comparator == 1 - if !comparison_value.to_s.split(",").map { |x| x.strip() }.include?(user_value.to_s) - log_entries.push(format_match_rule(comparison_attribute, user_value, comparator, comparison_value, value)) - return value, variation_id, rollout_rule, nil, nil - end - # CONTAINS - elsif comparator == 2 - if user_value.to_s.include?(comparison_value.to_s) - log_entries.push(format_match_rule(comparison_attribute, user_value, comparator, comparison_value, value)) - return value, variation_id, rollout_rule, nil, nil - end - # DOES NOT CONTAIN - elsif comparator == 3 - if !user_value.to_s.include?(comparison_value.to_s) - log_entries.push(format_match_rule(comparison_attribute, user_value, comparator, comparison_value, value)) - return value, variation_id, rollout_rule, nil, nil - end - # IS ONE OF, IS NOT ONE OF (Semantic version) - elsif (4 <= comparator) && (comparator <= 5) - begin - match = false - user_value_version = Semantic::Version.new(user_value.to_s.strip()) - ((comparison_value.to_s.split(",").map { |x| x.strip() }).reject { |c| c.empty? }).each { |x| - version = Semantic::Version.new(x) - match = (user_value_version == version) || match - } - if match && comparator == 4 || !match && comparator == 5 - log_entries.push(format_match_rule(comparison_attribute, user_value, comparator, comparison_value, value)) - return value, variation_id, rollout_rule, nil, nil + # Evaluate targeting rule conditions (logically connected by AND) + if evaluate_conditions(conditions, context, salt, config, log_builder, value) + if served_value + variation_id = served_value[VARIATION_ID] || default_variation_id + log_builder.new_line("Returning '#{value}'.") if log_builder && is_root_flag_evaluation + return [value, variation_id, targeting_rule, nil, nil] end - rescue ArgumentError => e - message = format_validation_error_rule(comparison_attribute, user_value, comparator, comparison_value, e.to_s) - @log.warn(0, message) - log_entries.push(message) + else next end - # LESS THAN, LESS THAN OR EQUALS TO, GREATER THAN, GREATER THAN OR EQUALS TO (Semantic version) - elsif (6 <= comparator) && (comparator <= 9) - begin - user_value_version = Semantic::Version.new(user_value.to_s.strip()) - comparison_value_version = Semantic::Version.new(comparison_value.to_s.strip()) - if (comparator == 6 && user_value_version < comparison_value_version) || - (comparator == 7 && user_value_version <= comparison_value_version) || - (comparator == 8 && user_value_version > comparison_value_version) || - (comparator == 9 && user_value_version >= comparison_value_version) - log_entries.push(format_match_rule(comparison_attribute, user_value, comparator, comparison_value, value)) - return value, variation_id, rollout_rule, nil, nil - end - rescue ArgumentError => e - message = format_validation_error_rule(comparison_attribute, user_value, comparator, comparison_value, e.to_s) - @log.warn(0, message) - log_entries.push(message) - next - end - elsif (10 <= comparator) && (comparator <= 15) - begin - user_value_float = Float(user_value.to_s.gsub(",", ".")) - comparison_value_float = Float(comparison_value.to_s.gsub(",", ".")) - if (comparator == 10 && user_value_float == comparison_value_float) || - (comparator == 11 && user_value_float != comparison_value_float) || - (comparator == 12 && user_value_float < comparison_value_float) || - (comparator == 13 && user_value_float <= comparison_value_float) || - (comparator == 14 && user_value_float > comparison_value_float) || - (comparator == 15 && user_value_float >= comparison_value_float) - log_entries.push(format_match_rule(comparison_attribute, user_value, comparator, comparison_value, value)) - return value, variation_id, rollout_rule, nil, nil - end - rescue Exception => e - message = format_validation_error_rule(comparison_attribute, user_value, comparator, comparison_value, e.to_s) - @log.warn(0, message) - log_entries.push(message) - next + end + + # Evaluate percentage options of the targeting rule + log_builder&.increase_indent + percentage_options = targeting_rule.fetch(TARGETING_RULE_PERCENTAGE_OPTIONS, []) + percentage_evaluation_result, percentage_value, percentage_variation_id, percentage_option = + evaluate_percentage_options(percentage_options, context, percentage_rule_attribute, + default_variation_id, log_builder) + + if percentage_evaluation_result + if log_builder + log_builder.decrease_indent + log_builder.new_line("Returning '#{percentage_value}'.") if is_root_flag_evaluation end - # IS ONE OF (Sensitive) - elsif comparator == 16 - if comparison_value.to_s.split(",").map { |x| x.strip() }.include?(Digest::SHA1.hexdigest(user_value).to_s) - log_entries.push(format_match_rule(comparison_attribute, user_value, comparator, comparison_value, value)) - return value, variation_id, rollout_rule, nil, nil + return [percentage_value, percentage_variation_id, targeting_rule, percentage_option, nil] + else + if log_builder + log_builder.new_line('The current targeting rule is ignored and the evaluation continues with the next rule.') + log_builder.decrease_indent end - # IS NOT ONE OF (Sensitive) - elsif comparator == 17 - if !comparison_value.to_s.split(",").map { |x| x.strip() }.include?(Digest::SHA1.hexdigest(user_value).to_s) - log_entries.push(format_match_rule(comparison_attribute, user_value, comparator, comparison_value, value)) - return value, variation_id, rollout_rule, nil, nil + next + end + end + + # Evaluate percentage options + percentage_options = setting_descriptor.fetch(PERCENTAGE_OPTIONS, []) + percentage_evaluation_result, percentage_value, percentage_variation_id, percentage_option = + evaluate_percentage_options(percentage_options, context, percentage_rule_attribute, + default_variation_id, log_builder) + + if percentage_evaluation_result + log_builder.new_line("Returning '#{percentage_value}'.") if log_builder && is_root_flag_evaluation + return [percentage_value, percentage_variation_id, nil, percentage_option, nil] + end + + return_value = Config.get_value(setting_descriptor, setting_type) + return_variation_id = setting_descriptor.fetch(VARIATION_ID, default_variation_id) + log_builder.new_line("Returning '#{return_value}'.") if log_builder && is_root_flag_evaluation + return [return_value, return_variation_id, nil, nil, nil] + rescue => e + # During the recursive evaluation of a prerequisite flag, we propagate the exceptions + # and let the root flag's evaluation code handle them. + if !is_root_flag_evaluation + raise e + else + error = "Failed to evaluate setting '#{key}'. (#{e}). " \ + "Returning the `%s` parameter that you specified in your application: '#{default_value}'." + @log.error(2001, error) + return [default_value, default_variation_id, nil, nil, error] + end + end + end + + private + + # Calculates the SHA256 hash of the given value with the given salt and context_salt. + def sha256(value_utf8, salt, context_salt) + Digest::SHA256.hexdigest(value_utf8 + salt + context_salt) + end + + def format_rule(comparison_attribute, comparator, comparison_value) + comparator_text = COMPARATOR_TEXTS[comparator] + "User.#{comparison_attribute} #{comparator_text} #{EvaluationLogBuilder.trunc_comparison_value_if_needed(comparator, comparison_value)}" + end + + def user_attribute_value_to_string(value) + return nil if value.nil? + + if value.is_a?(DateTime) || value.is_a?(Time) + value = get_user_attribute_value_as_seconds_since_epoch(value) + elsif Utils.is_string_list(value) + value = get_user_attribute_value_as_string_list(value) + return value.to_json # Convert the array to a JSON string + end + + if value.is_a?(Float) + return 'NaN' if value.nan? + return 'Infinity' if value.infinite? == 1 + return '-Infinity' if value.infinite? == -1 + return value.to_s if value.to_s.include?('e') + return value.to_i.to_s if value == value.to_i + end + + value.to_s + end + + def get_user_attribute_value_as_text(attribute_name, attribute_value, condition, key) + return attribute_value if attribute_value.is_a?(String) + + @log.warn(3005, "Evaluation of condition (#{condition}) for setting '#{key}' may not produce the expected result " \ + "(the User.#{attribute_name} attribute is not a string value, thus it was automatically converted to " \ + "the string value '#{attribute_value}'). Please make sure that using a non-string value was intended.") + user_attribute_value_to_string(attribute_value) + end + + def convert_numeric_to_float(value) + if value.is_a?(String) + value = value.tr(',', '.').strip + if value == 'NaN' + return Float::NAN + elsif value == 'Infinity' + return Float::INFINITY + elsif value == '-Infinity' + return -Float::INFINITY + end + end + + Float(value) + end + + def get_user_attribute_value_as_seconds_since_epoch(attribute_value) + if attribute_value.is_a?(DateTime) || attribute_value.is_a?(Time) + return Utils.get_seconds_since_epoch(attribute_value) + end + + convert_numeric_to_float(attribute_value) + end + + def get_user_attribute_value_as_string_list(attribute_value) + if attribute_value.is_a?(String) + attribute_value_list = JSON.parse(attribute_value) + else + attribute_value_list = attribute_value + end + + raise "All items in the list must be strings" unless Utils.is_string_list(attribute_value_list) + + attribute_value_list + end + + # :returns evaluation error message + def handle_invalid_user_attribute(comparison_attribute, comparator, comparison_value, key, validation_error) + error = "cannot evaluate, the User.#{comparison_attribute} attribute is invalid (#{validation_error})" + formatted_rule = format_rule(comparison_attribute, comparator, comparison_value) + @log.warn(3004, "Cannot evaluate condition (#{formatted_rule}) for setting '#{key}' " \ + "(#{validation_error}). Please check the User.#{comparison_attribute} attribute and make sure that its value corresponds to the " \ + "comparison operator.") + error + end + + # :returns evaluation_result, percentage_value, percentage_variation_id, percentage_option + def evaluate_percentage_options(percentage_options, context, percentage_rule_attribute, default_variation_id, log_builder) + return [false, nil, nil, nil] if percentage_options.empty? + + user = context.user + key = context.key + + if user.nil? + unless context.is_missing_user_object_logged + @log.warn(3001, "Cannot evaluate targeting rules and % options for setting '#{key}' " \ + "(User Object is missing). " \ + "You should pass a User Object to the evaluation methods like `get_value()` " \ + "in order to make targeting work properly. " \ + "Read more: https://configcat.com/docs/advanced/user-object/") + context.is_missing_user_object_logged = true + end + + log_builder&.new_line('Skipping % options because the User Object is missing.') + return [false, nil, nil, nil] + end + + user_attribute_name = percentage_rule_attribute || 'Identifier' + if percentage_rule_attribute + user_key = user.get_attribute(percentage_rule_attribute) + else + user_key = user.get_identifier + end + + if percentage_rule_attribute && user_key.nil? + unless context.is_missing_user_object_attribute_logged + @log.warn(3003, "Cannot evaluate % options for setting '#{key}' " \ + "(the User.#{percentage_rule_attribute} attribute is missing). You should set the User.#{percentage_rule_attribute} attribute in order to make " \ + "targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/") + context.is_missing_user_object_attribute_logged = true + end + + log_builder&.new_line("Skipping % options because the User.#{user_attribute_name} attribute is missing.") + return [false, nil, nil, nil] + end + + hash_candidate = "#{key}#{user_attribute_value_to_string(user_key)}".encode("utf-8") + hash_val = Digest::SHA1.hexdigest(hash_candidate)[0...7].to_i(16) % 100 + + if log_builder + log_builder.new_line("Evaluating % options based on the User.#{user_attribute_name} attribute:") + log_builder.new_line("- Computing hash in the [0..99] range from User.#{user_attribute_name} => #{hash_val} " \ + "(this value is sticky and consistent across all SDKs)") + end + + bucket = 0 + index = 1 + percentage_options.each do |percentage_option| + percentage = percentage_option[PERCENTAGE] || 0 + bucket += percentage + if hash_val < bucket + percentage_value = Config.get_value(percentage_option, context.setting_type) + variation_id = percentage_option[VARIATION_ID] || default_variation_id + if log_builder + log_builder.new_line("- Hash value #{hash_val} selects % option #{index} (#{percentage}%), '#{percentage_value}'.") + end + return [true, percentage_value, variation_id, percentage_option] + end + index += 1 + end + + [false, nil, nil, nil] + end + + def evaluate_conditions(conditions, context, salt, config, log_builder, value) + first_condition = true + condition_result = true + error = nil + + conditions.each do |condition| + user_condition = condition[USER_CONDITION] + segment_condition = condition[SEGMENT_CONDITION] + prerequisite_flag_condition = condition[PREREQUISITE_FLAG_CONDITION] + + if first_condition + log_builder&.new_line('- IF ') + log_builder&.increase_indent + first_condition = false + else + log_builder&.new_line('AND ') + end + + if user_condition + result, error = evaluate_user_condition(user_condition, context, context.key, salt, log_builder) + if log_builder && conditions.size > 1 + log_builder.append("=> #{result ? 'true' : 'false'}") + log_builder.append(', skipping the remaining AND conditions') unless result + end + + if !result || error + condition_result = false + break + end + elsif segment_condition + result, error = evaluate_segment_condition(segment_condition, context, salt, log_builder) + if log_builder + if conditions.size > 1 + log_builder.append(' ') if error.nil? + log_builder.append("=> #{result ? 'true' : 'false'}") + log_builder.append(', skipping the remaining AND conditions') unless result + elsif error.nil? + log_builder.new_line end end - log_entries.push(format_no_match_rule(comparison_attribute, user_value, comparator, comparison_value)) - end - - if rollout_percentage_items.size > 0 - user_key = user.get_identifier() - hash_candidate = ("%s%s" % [key, user_key]).encode("utf-8") - hash_val = Digest::SHA1.hexdigest(hash_candidate)[0...7].to_i(base = 16) % 100 - bucket = 0 - for rollout_percentage_item in rollout_percentage_items || [] - bucket += rollout_percentage_item.fetch(PERCENTAGE, 0) - if hash_val < bucket - percentage_value = rollout_percentage_item.fetch(VALUE, nil) - variation_id = rollout_percentage_item.fetch(VARIATION_ID, default_variation_id) - log_entries.push("Evaluating %% options. Returning %s" % percentage_value) - return percentage_value, variation_id, nil, rollout_percentage_item, nil + + if !result || error + condition_result = false + break + end + elsif prerequisite_flag_condition + result = evaluate_prerequisite_flag_condition(prerequisite_flag_condition, context, config, log_builder) + if log_builder + if conditions.size > 1 + log_builder.append(" => #{result ? 'true' : 'false'}") + log_builder.append(', skipping the remaining AND conditions') unless result + elsif error.nil? + log_builder.new_line end end + + if !result + condition_result = false + break + end end - return_value = setting_descriptor.fetch(VALUE, default_value) - return_variation_id = setting_descriptor.fetch(VARIATION_ID, default_variation_id) - log_entries.push("Returning %s" % return_value) - return return_value, return_variation_id, nil, nil, nil - ensure - @log.info(5000, log_entries.join("\n")) end + + if log_builder + log_builder.new_line if conditions.size > 1 + if error + log_builder.append("THEN #{value ? "'#{value}'" : '% options'} => #{error}") + log_builder.new_line("The current targeting rule is ignored and the evaluation continues with the next rule.") + else + log_builder.append("THEN #{value ? "'#{value}'" : "% options"} => #{condition_result ? "MATCH, applying rule" : "no match"}") + end + log_builder.decrease_indent if conditions.size > 0 + end + + condition_result end - private + def evaluate_prerequisite_flag_condition(prerequisite_flag_condition, context, config, log_builder) + prerequisite_key = prerequisite_flag_condition[PREREQUISITE_FLAG_KEY] + prerequisite_comparator = prerequisite_flag_condition[PREREQUISITE_COMPARATOR] + + # Check if the prerequisite key exists + settings = config.fetch(FEATURE_FLAGS, {}) + if prerequisite_key.nil? || settings[prerequisite_key].nil? + raise "Prerequisite flag key is missing or invalid." + end + + prerequisite_condition_result = false + prerequisite_flag_setting_type = settings[prerequisite_key][SETTING_TYPE] + prerequisite_comparison_value_type = Config.get_value_type(prerequisite_flag_condition) + + prerequisite_comparison_value = Config.get_value(prerequisite_flag_condition, prerequisite_flag_setting_type) + + # Type mismatch check + if prerequisite_comparison_value_type != SettingType.to_type(prerequisite_flag_setting_type) + raise "Type mismatch between comparison value '#{prerequisite_comparison_value}' and prerequisite flag '#{prerequisite_key}'" + end + + prerequisite_condition = "Flag '#{prerequisite_key}' #{PREREQUISITE_COMPARATOR_TEXTS[prerequisite_comparator]} '#{prerequisite_comparison_value}'" + + # Circular dependency check + visited_keys = context.visited_keys + visited_keys.push(context.key) + if visited_keys.include?(prerequisite_key) + depending_flags = visited_keys.push(prerequisite_key).map { |s| "'#{s}'" }.join(' -> ') + raise "Circular dependency detected between the following depending flags: #{depending_flags}." + end + + if log_builder + log_builder.append(prerequisite_condition) + log_builder.new_line('(').increase_indent + log_builder.new_line("Evaluating prerequisite flag '#{prerequisite_key}':") + end + + prerequisite_value, _, _, _, _ = evaluate(key: prerequisite_key, user: context.user, default_value: nil, default_variation_id: nil, + config: config, log_builder: log_builder, visited_keys: context.visited_keys) + + visited_keys.pop + + if log_builder + log_builder.new_line("Prerequisite flag evaluation result: '#{prerequisite_value}'.") + log_builder.new_line("Condition (Flag '#{prerequisite_key}' #{PREREQUISITE_COMPARATOR_TEXTS[prerequisite_comparator]} '#{prerequisite_comparison_value}') evaluates to ") + end + + if prerequisite_comparator == PrerequisiteComparator::EQUALS + prerequisite_condition_result = true if prerequisite_value == prerequisite_comparison_value + elsif prerequisite_comparator == PrerequisiteComparator::NOT_EQUALS + prerequisite_condition_result = true if prerequisite_value != prerequisite_comparison_value + else + raise "Comparison operator is missing or invalid." + end + + if log_builder + log_builder.append("#{prerequisite_condition_result ? 'true' : 'false'}.") + log_builder.decrease_indent()&.new_line(')') + end - def format_match_rule(comparison_attribute, user_value, comparator, comparison_value, value) - return "Evaluating rule: [%s:%s] [%s] [%s] => match, returning: %s" % [comparison_attribute, user_value, COMPARATOR_TEXTS[comparator], comparison_value, value] + prerequisite_condition_result end - def format_no_match_rule(comparison_attribute, user_value, comparator, comparison_value) - return "Evaluating rule: [%s:%s] [%s] [%s] => no match" % [comparison_attribute, user_value, COMPARATOR_TEXTS[comparator], comparison_value] + def evaluate_segment_condition(segment_condition, context, salt, log_builder) + user = context.user + key = context.key + + segment = segment_condition[INLINE_SEGMENT] + if segment.nil? + raise 'Segment reference is invalid.' + end + + segment_name = segment.fetch(SEGMENT_NAME, '') + segment_comparator = segment_condition[SEGMENT_COMPARATOR] + segment_conditions = segment.fetch(SEGMENT_CONDITIONS, []) + + if user.nil? + unless context.is_missing_user_object_logged + @log.warn(3001, "Cannot evaluate targeting rules and % options for setting '#{key}' " \ + "(User Object is missing). " \ + "You should pass a User Object to the evaluation methods like `get_value()` " \ + "in order to make targeting work properly. " \ + "Read more: https://configcat.com/docs/advanced/user-object/") + context.is_missing_user_object_logged = true + end + log_builder&.append("User #{SEGMENT_COMPARATOR_TEXTS[segment_comparator]} '#{segment_name}' ") + return [false, "cannot evaluate, User Object is missing"] + end + + # IS IN SEGMENT, IS NOT IN SEGMENT + if [SegmentComparator::IS_IN, SegmentComparator::IS_NOT_IN].include?(segment_comparator) + if log_builder + log_builder.append("User #{SEGMENT_COMPARATOR_TEXTS[segment_comparator]} '#{segment_name}'") + log_builder.new_line("(").increase_indent + log_builder.new_line("Evaluating segment '#{segment_name}':") + end + + # Set initial condition result based on comparator + segment_condition_result = segment_comparator == SegmentComparator::IS_IN + + # Evaluate segment conditions (logically connected by AND) + first_segment_rule = true + error = nil + segment_conditions.each do |segment_condition| + if first_segment_rule + if log_builder + log_builder.new_line('- IF ') + log_builder.increase_indent + end + first_segment_rule = false + else + log_builder&.new_line('AND ') + end + + result, error = evaluate_user_condition(segment_condition, context, segment_name, salt, log_builder) + if log_builder + log_builder.append("=> #{result ? 'true' : 'false'}") + log_builder.append(', skipping the remaining AND conditions') unless result + end + + unless result + segment_condition_result = segment_comparator == SegmentComparator::IS_IN ? false : true + break + end + end + + if log_builder + log_builder.decrease_indent + segment_evaluation_result = segment_comparator == SegmentComparator::IS_IN ? segment_condition_result : !segment_condition_result + log_builder.new_line("Segment evaluation result: ") + unless error + log_builder.append("User IS#{segment_evaluation_result ? ' ' : ' NOT '}IN SEGMENT.") + else + log_builder.append("#{error}.") + end + + log_builder.new_line("Condition (User #{SEGMENT_COMPARATOR_TEXTS[segment_comparator]} '#{segment_name}') ") + + unless error + log_builder.append("evaluates to #{segment_condition_result ? 'true' : 'false'}.") + else + log_builder.append("failed to evaluate.") + end + + log_builder.decrease_indent.new_line(')') + log_builder.new_line if error + end + + return [segment_condition_result, error] + end + + raise "Comparison operator is missing or invalid." end - def format_validation_error_rule(comparison_attribute, user_value, comparator, comparison_value, error) - return "Evaluating rule: [%s:%s] [%s] [%s] => SKIP rule. Validation error: %s" % [comparison_attribute, user_value, COMPARATOR_TEXTS[comparator], comparison_value, error] + # :returns result of user condition, error + def evaluate_user_condition(user_condition, context, context_salt, salt, log_builder) + user = context.user + key = context.key + + comparison_attribute = user_condition[COMPARISON_ATTRIBUTE] + comparator = user_condition[COMPARATOR] + comparison_value = user_condition[COMPARISON_VALUES[comparator]] + condition = format_rule(comparison_attribute, comparator, comparison_value) + error = nil + + if comparison_attribute.nil? + raise "Comparison attribute name is missing." + end + + log_builder&.append("#{condition} ") + + if user.nil? + unless context.is_missing_user_object_logged + @log.warn(3001, "Cannot evaluate targeting rules and % options for setting '#{key}' " \ + "(User Object is missing). " \ + "You should pass a User Object to the evaluation methods like `get_value()` " \ + "in order to make targeting work properly. " \ + "Read more: https://configcat.com/docs/advanced/user-object/") + context.is_missing_user_object_logged = true + end + error = "cannot evaluate, User Object is missing" + return [false, error] + end + + user_value = user.get_attribute(comparison_attribute) + if user_value.nil? || (user_value.is_a?(String) && user_value.empty?) + @log.warn(3003, "Cannot evaluate condition (#{condition}) for setting '#{key}' " \ + "(the User.#{comparison_attribute} attribute is missing). You should set the User.#{comparison_attribute} attribute in order to make " \ + "targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/") + error = "cannot evaluate, the User.#{comparison_attribute} attribute is missing" + return [false, error] + end + + # IS ONE OF + if comparator == Comparator::IS_ONE_OF + user_value = get_user_attribute_value_as_text(comparison_attribute, user_value, condition, key) + return true, error if comparison_value.include?(user_value) + # IS NOT ONE OF + elsif comparator == Comparator::IS_NOT_ONE_OF + user_value = get_user_attribute_value_as_text(comparison_attribute, user_value, condition, key) + return true, error unless comparison_value.include?(user_value) + # CONTAINS ANY OF + elsif comparator == Comparator::CONTAINS_ANY_OF + user_value = get_user_attribute_value_as_text(comparison_attribute, user_value, condition, key) + comparison_value.each do |comparison| + return true, error if user_value.include?(comparison) + end + # NOT CONTAINS ANY OF + elsif comparator == Comparator::NOT_CONTAINS_ANY_OF + user_value = get_user_attribute_value_as_text(comparison_attribute, user_value, condition, key) + return true, error unless comparison_value.any? { |comparison| user_value.include?(comparison) } + # IS ONE OF, IS NOT ONE OF (Semantic version) + elsif comparator >= Comparator::IS_ONE_OF_SEMVER && comparator <= Comparator::IS_NOT_ONE_OF_SEMVER + begin + match = false + user_value_version = Semantic::Version.new(user_value.to_s.strip()) + ((comparison_value.map { |x| x.strip() }).reject { |c| c.empty? }).each { |x| + version = Semantic::Version.new(x) + match = (user_value_version == version) || match + } + if match && comparator == Comparator::IS_ONE_OF_SEMVER || !match && comparator == Comparator::IS_NOT_ONE_OF_SEMVER + return true, error + end + rescue ArgumentError => e + validation_error = "'#{user_value.to_s.strip}' is not a valid semantic version" + error = handle_invalid_user_attribute(comparison_attribute, comparator, comparison_value, key, validation_error) + return false, error + end + # LESS THAN, LESS THAN OR EQUAL TO, GREATER THAN, GREATER THAN OR EQUAL TO (Semantic version) + elsif comparator >= Comparator::LESS_THAN_SEMVER && comparator <= Comparator::GREATER_THAN_OR_EQUAL_SEMVER + begin + user_value_version = Semantic::Version.new(user_value.to_s.strip) + comparison_value_version = Semantic::Version.new(comparison_value.to_s.strip) + if (comparator == Comparator::LESS_THAN_SEMVER && user_value_version < comparison_value_version) || + (comparator == Comparator::LESS_THAN_OR_EQUAL_SEMVER && user_value_version <= comparison_value_version) || + (comparator == Comparator::GREATER_THAN_SEMVER && user_value_version > comparison_value_version) || + (comparator == Comparator::GREATER_THAN_OR_EQUAL_SEMVER && user_value_version >= comparison_value_version) + return true, error + end + rescue ArgumentError => e + validation_error = "'#{user_value.to_s.strip}' is not a valid semantic version" + error = handle_invalid_user_attribute(comparison_attribute, comparator, comparison_value, key, validation_error) + return false, error + end + # =, <>, <, <=, >, >= (number) + elsif comparator >= Comparator::EQUALS_NUMBER && comparator <= Comparator::GREATER_THAN_OR_EQUAL_NUMBER + begin + user_value_float = convert_numeric_to_float(user_value) + rescue Exception => e + validation_error = "'#{user_value}' is not a valid decimal number" + error = handle_invalid_user_attribute(comparison_attribute, comparator, comparison_value, key, validation_error) + return false, error + end + + comparison_value_float = Float(comparison_value) + if (comparator == Comparator::EQUALS_NUMBER && user_value_float == comparison_value_float) || + (comparator == Comparator::NOT_EQUALS_NUMBER && user_value_float != comparison_value_float) || + (comparator == Comparator::LESS_THAN_NUMBER && user_value_float < comparison_value_float) || + (comparator == Comparator::LESS_THAN_OR_EQUAL_NUMBER && user_value_float <= comparison_value_float) || + (comparator == Comparator::GREATER_THAN_NUMBER && user_value_float > comparison_value_float) || + (comparator == Comparator::GREATER_THAN_OR_EQUAL_NUMBER && user_value_float >= comparison_value_float) + return true, error + end + # IS ONE OF (hashed) + elsif comparator == Comparator::IS_ONE_OF_HASHED + user_value = get_user_attribute_value_as_text(comparison_attribute, user_value, condition, key) + if comparison_value.include?(sha256(user_value, salt, context_salt)) + return true, error + end + # IS NOT ONE OF (hashed) + elsif comparator == Comparator::IS_NOT_ONE_OF_HASHED + user_value = get_user_attribute_value_as_text(comparison_attribute, user_value, condition, key) + unless comparison_value.include?(sha256(user_value, salt, context_salt)) + return true, error + end + # BEFORE, AFTER (UTC datetime) + elsif comparator >= Comparator::BEFORE_DATETIME && comparator <= Comparator::AFTER_DATETIME + begin + user_value_float = get_user_attribute_value_as_seconds_since_epoch(user_value) + rescue ArgumentError => e + validation_error = "'#{user_value}' is not a valid Unix timestamp (number of seconds elapsed since Unix epoch)" + error = handle_invalid_user_attribute(comparison_attribute, comparator, comparison_value, key, validation_error) + return false, error + end + + comparison_value_float = Float(comparison_value) + if (comparator == Comparator::BEFORE_DATETIME && user_value_float < comparison_value_float) || + (comparator == Comparator::AFTER_DATETIME && user_value_float > comparison_value_float) + return true, error + end + # EQUALS (hashed) + elsif comparator == Comparator::EQUALS_HASHED + user_value = get_user_attribute_value_as_text(comparison_attribute, user_value, condition, key) + if sha256(user_value, salt, context_salt) == comparison_value + return true, error + end + # NOT EQUALS (hashed) + elsif comparator == Comparator::NOT_EQUALS_HASHED + user_value = get_user_attribute_value_as_text(comparison_attribute, user_value, condition, key) + if sha256(user_value, salt, context_salt) != comparison_value + return true, error + end + # STARTS WITH ANY OF, NOT STARTS WITH ANY OF, ENDS WITH ANY OF, NOT ENDS WITH ANY OF (hashed) + elsif comparator >= Comparator::STARTS_WITH_ANY_OF_HASHED && comparator <= Comparator::NOT_ENDS_WITH_ANY_OF_HASHED + comparison_value.each do |comparison| + underscore_index = comparison.index('_') + length = comparison[0...underscore_index].to_i + user_value = get_user_attribute_value_as_text(comparison_attribute, user_value, condition, key) + + if user_value.bytesize >= length + comparison_string = comparison[(underscore_index + 1)..-1] + if (comparator == Comparator::STARTS_WITH_ANY_OF_HASHED && sha256(user_value.byteslice(0...length), salt, context_salt) == comparison_string) || + (comparator == Comparator::ENDS_WITH_ANY_OF_HASHED && sha256(user_value.byteslice(-length..-1), salt, context_salt) == comparison_string) + return true, error + elsif (comparator == Comparator::NOT_STARTS_WITH_ANY_OF_HASHED && sha256(user_value.byteslice(0...length), salt, context_salt) == comparison_string) || + (comparator == Comparator::NOT_ENDS_WITH_ANY_OF_HASHED && sha256(user_value.byteslice(-length..-1), salt, context_salt) == comparison_string) + return false, nil + end + end + end + + # If no matches were found for the NOT_* conditions, then return true + if [Comparator::NOT_STARTS_WITH_ANY_OF_HASHED, Comparator::NOT_ENDS_WITH_ANY_OF_HASHED].include?(comparator) + return true, error + end + # ARRAY CONTAINS ANY OF, ARRAY NOT CONTAINS ANY OF (hashed) + elsif comparator >= Comparator::ARRAY_CONTAINS_ANY_OF_HASHED && comparator <= Comparator::ARRAY_NOT_CONTAINS_ANY_OF_HASHED + begin + user_value_list = get_user_attribute_value_as_string_list(user_value) + rescue Exception + validation_error = "'#{user_value}' is not a valid string array" + error = handle_invalid_user_attribute(comparison_attribute, comparator, comparison_value, key, validation_error) + return false, error + end + + hashed_user_values = user_value_list.map { |x| sha256(x, salt, context_salt) } + if comparator == Comparator::ARRAY_CONTAINS_ANY_OF_HASHED + comparison_value.each do |comparison| + return true, error if hashed_user_values.include?(comparison) + end + else + comparison_value.each do |comparison| + return false, nil if hashed_user_values.include?(comparison) + end + return true, error + end + # EQUALS + elsif comparator == Comparator::EQUALS + user_value = get_user_attribute_value_as_text(comparison_attribute, user_value, condition, key) + return true, error if user_value == comparison_value + # NOT EQUALS + elsif comparator == Comparator::NOT_EQUALS + user_value = get_user_attribute_value_as_text(comparison_attribute, user_value, condition, key) + return true, error if user_value != comparison_value + # STARTS WITH ANY OF, NOT STARTS WITH ANY OF, ENDS WITH ANY OF, NOT ENDS WITH ANY OF + elsif comparator >= Comparator::STARTS_WITH_ANY_OF && comparator <= Comparator::NOT_ENDS_WITH_ANY_OF + user_value = get_user_attribute_value_as_text(comparison_attribute, user_value, condition, key) + comparison_value.each do |comparison| + if (comparator == Comparator::STARTS_WITH_ANY_OF && user_value.start_with?(comparison)) || + (comparator == Comparator::ENDS_WITH_ANY_OF && user_value.end_with?(comparison)) + return true, error + elsif (comparator == Comparator::NOT_STARTS_WITH_ANY_OF && user_value.start_with?(comparison)) || + (comparator == Comparator::NOT_ENDS_WITH_ANY_OF && user_value.end_with?(comparison)) + return false, nil + end + end + + # If no matches were found for the NOT_* conditions, then return true + if [Comparator::NOT_STARTS_WITH_ANY_OF, Comparator::NOT_ENDS_WITH_ANY_OF].include?(comparator) + return true, error + end + # ARRAY CONTAINS ANY OF, ARRAY NOT CONTAINS ANY OF + elsif comparator >= Comparator::ARRAY_CONTAINS_ANY_OF && comparator <= Comparator::ARRAY_NOT_CONTAINS_ANY_OF + begin + user_value_list = get_user_attribute_value_as_string_list(user_value) + rescue Exception + validation_error = "'#{user_value}' is not a valid string array" + error = handle_invalid_user_attribute(comparison_attribute, comparator, comparison_value, key, validation_error) + return false, error + end + + if comparator == Comparator::ARRAY_CONTAINS_ANY_OF + comparison_value.each do |comparison| + return true, error if user_value_list.include?(comparison) + end + else + comparison_value.each do |comparison| + return false, nil if user_value_list.include?(comparison) + end + return true, error + end + else + raise "Comparison operator is missing or invalid." + end + + [false, nil] end end end diff --git a/lib/configcat/user.rb b/lib/configcat/user.rb index 9bbb692..04b1810 100644 --- a/lib/configcat/user.rb +++ b/lib/configcat/user.rb @@ -1,10 +1,45 @@ module ConfigCat - # The user object for variation evaluation + # User Object. Contains user attributes which are used for evaluating targeting rules and percentage options. class User PREDEFINED = ["Identifier", "Email", "Country"] attr_reader :identifier + # Initialize a User object. + # Args: + # identifier: The unique identifier of the user or session (e.g. email address, primary key, session ID, etc.) + # email: Email address of the user. + # country: Country of the user. + # custom: Custom attributes of the user for advanced targeting rule definitions (e.g. role, subscription type, etc.) + # All comparators support string values as User Object attribute (in some cases they need to be provided in a + # specific format though, see below), but some of them also support other types of values. It depends on the + # comparator how the values will be handled. The following rules apply: + # Text-based comparators (EQUALS, IS_ONE_OF, etc.) + # * accept string values, + # * all other values are automatically converted to string + # (a warning will be logged but evaluation will continue as normal). + # SemVer-based comparators (IS_ONE_OF_SEMVER, LESS_THAN_SEMVER, GREATER_THAN_SEMVER, etc.) + # * accept string values containing a properly formatted, valid semver value, + # * all other values are considered invalid + # (a warning will be logged and the currently evaluated targeting rule will be skipped). + # Number-based comparators (EQUALS_NUMBER, LESS_THAN_NUMBER, GREATER_THAN_OR_EQUAL_NUMBER, etc.) + # * accept float values and all other numeric values which can safely be converted to float, + # * accept string values containing a properly formatted, valid float value, + # * all other values are considered invalid + # (a warning will be logged and the currently evaluated targeting rule will be skipped). + # Date time-based comparators (BEFORE_DATETIME / AFTER_DATETIME) + # * accept datetime values, which are automatically converted to a second-based Unix timestamp + # (datetime values with naive timezone are considered to be in UTC), + # * accept float values representing a second-based Unix timestamp + # and all other numeric values which can safely be converted to float, + # * accept string values containing a properly formatted, valid float value, + # * all other values are considered invalid + # (a warning will be logged and the currently evaluated targeting rule will be skipped). + # String array-based comparators (ARRAY_CONTAINS_ANY_OF / ARRAY_NOT_CONTAINS_ANY_OF) + # * accept arrays of strings, + # * accept string values containing a valid JSON string which can be deserialized to an array of strings, + # * all other values are considered invalid + # (a warning will be logged and the currently evaluated targeting rule will be skipped). def initialize(identifier, email: nil, country: nil, custom: nil) @identifier = (!identifier.equal?(nil)) ? identifier : "" @data = { "Identifier" => identifier, "Email" => email, "Country" => country } @@ -26,10 +61,14 @@ def to_s dump = { 'Identifier': @identifier, 'Email': @data['Email'], - 'Country': @data['Country'], - 'Custom': @custom, + 'Country': @data['Country'] } - return dump.to_json + dump.merge!(@custom) if @custom + filtered_dump = dump.reject { |_, v| v.nil? } + formatted_dump = filtered_dump.transform_values do |value| + value.is_a?(DateTime) ? value.strftime('%Y-%m-%dT%H:%M:%S.%L%z') : value + end + return JSON.generate(formatted_dump, ascii_only: false, separators: %w[, :]) end end end diff --git a/lib/configcat/utils.rb b/lib/configcat/utils.rb index bb756c3..fde1ac1 100644 --- a/lib/configcat/utils.rb +++ b/lib/configcat/utils.rb @@ -3,8 +3,28 @@ class Utils DISTANT_FUTURE = Float::INFINITY DISTANT_PAST = 0 + def self.get_date_time(seconds_since_epoch) + Time.at(seconds_since_epoch).utc + end + def self.get_utc_now_seconds_since_epoch - return Time.now.utc.to_f + Time.now.utc.to_f + end + + def self.get_seconds_since_epoch(date_time) + date_time.to_time.to_f + end + + def self.is_string_list(value) + # Check if the value is an Array + return false unless value.is_a?(Array) + + # Check if all elements in the Array are Strings + value.each do |item| + return false unless item.is_a?(String) + end + + return true end end end diff --git a/lib/configcat/version.rb b/lib/configcat/version.rb index 954888a..34447dd 100644 --- a/lib/configcat/version.rb +++ b/lib/configcat/version.rb @@ -1,3 +1,3 @@ module ConfigCat - VERSION = "7.0.0" + VERSION = "8.0.0" end diff --git a/spec/config_spec.rb b/spec/config_spec.rb new file mode 100644 index 0000000..cc56337 --- /dev/null +++ b/spec/config_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper' +require 'configcat/localdictionarydatasource' +require 'configcat/localfiledatasource' +require 'tempfile' +require 'json' +require_relative 'configcat/mocks' + + +RSpec.describe 'Config test', type: :feature do + it "test_value_setting_type_is_missing" do + value_dictionary = { + 't' => 6, # unsupported setting type + 'v' => { + 'b' => true + } + } + setting_type = value_dictionary[SETTING_TYPE] + expect { Config.get_value(value_dictionary, setting_type) }.to raise_error("Unsupported setting type") + end + + it "test_value_setting_type_is_valid_but_return_value_is_missing" do + value_dictionary = { + 't' => 0, # boolean + 'v' => { + 's' => true # the wrong property is set ("b" should be set) + } + } + setting_type = value_dictionary[SETTING_TYPE] + expect { Config.get_value(value_dictionary, setting_type) }.to raise_error("Setting value is not of the expected type TrueClass") + end + + it "test_value_setting_type_is_valid_and_the_return_value_is_present_but_it_is_invalid" do + value_dictionary = { + 't' => 0, # boolean + 'v' => { + 'b' => 'true' # the value is a string instead of a boolean + } + } + setting_type = value_dictionary[SETTING_TYPE] + expect { Config.get_value(value_dictionary, setting_type) }.to raise_error("Setting value is not of the expected type TrueClass") + end +end diff --git a/spec/configcat/autopollingcachepolicy_spec.rb b/spec/configcat/autopollingcachepolicy_spec.rb index 9cc10c7..ad17a6d 100644 --- a/spec/configcat/autopollingcachepolicy_spec.rb +++ b/spec/configcat/autopollingcachepolicy_spec.rb @@ -13,8 +13,9 @@ logger = ConfigCatLogger.new(hooks) cache_policy = ConfigService.new("", polling_mode, hooks, config_fetcher, logger, config_cache, false) sleep(2) - settings, _ = cache_policy.get_settings - expect(settings.fetch("testKey").fetch(VALUE)).to eq "testValue" + config, _ = cache_policy.get_config + settings = config.fetch(FEATURE_FLAGS) + expect(settings.fetch("testKey").fetch(VALUE).fetch(STRING_VALUE)).to eq "testValue" cache_policy.close end @@ -27,8 +28,9 @@ logger = ConfigCatLogger.new(hooks) cache_policy = ConfigService.new("", polling_mode, hooks, config_fetcher, logger, config_cache, false) - settings, _ = cache_policy.get_settings - expect(settings.fetch("testKey").fetch(VALUE)).to eq "testValue" + config, _ = cache_policy.get_config + settings = config.fetch(FEATURE_FLAGS) + expect(settings.fetch("testKey").fetch(VALUE).fetch(STRING_VALUE)).to eq "testValue" cache_policy.close end @@ -41,10 +43,10 @@ hooks = Hooks.new logger = ConfigCatLogger.new(hooks) cache_policy = ConfigService.new("", polling_mode, hooks, config_fetcher, logger, config_cache, false) - settings, _ = cache_policy.get_settings + config, _ = cache_policy.get_config end_time = Time.now.utc elapsed_time = end_time - start_time - expect(settings).to be nil + expect(config).to be nil expect(elapsed_time).to be > 1 expect(elapsed_time).to be < 2 cache_policy.close @@ -60,8 +62,9 @@ cache_policy = ConfigService.new("", polling_mode, hooks, config_fetcher, logger, config_cache, false) sleep(3) expect(config_fetcher.get_call_count).to eq 2 - settings, _ = cache_policy.get_settings - expect(settings.fetch("testKey").fetch(VALUE)).to eq "testValue" + config, _ = cache_policy.get_config + settings = config.fetch(FEATURE_FLAGS) + expect(settings.fetch("testKey").fetch(VALUE).fetch(STRING_VALUE)).to eq "testValue" cache_policy.close end @@ -74,13 +77,15 @@ logger = ConfigCatLogger.new(hooks) cache_policy = ConfigService.new("", polling_mode, hooks, config_fetcher, logger, config_cache, false) - settings, _ = cache_policy.get_settings - expect(settings.fetch("testKey").fetch(VALUE)).to eq 1 + config, _ = cache_policy.get_config + settings = config.fetch(FEATURE_FLAGS) + expect(settings.fetch("testKey").fetch(VALUE).fetch(INT_VALUE)).to eq 1 sleep(2.2) - settings, _ = cache_policy.get_settings - expect(settings.fetch("testKey").fetch(VALUE)).to eq 2 + config, _ = cache_policy.get_config + settings = config.fetch(FEATURE_FLAGS) + expect(settings.fetch("testKey").fetch(VALUE).fetch(INT_VALUE)).to eq 2 cache_policy.close end @@ -95,8 +100,8 @@ cache_policy = ConfigService.new("", polling_mode, hooks, config_fetcher, logger, config_cache, false) # Get value from Config Store, which indicates a config_fetcher call - settings, _ = cache_policy.get_settings - expect(settings).to be nil + config, _ = cache_policy.get_config + expect(config).to be nil cache_policy.close end @@ -109,11 +114,13 @@ logger = ConfigCatLogger.new(hooks) cache_policy = ConfigService.new("", polling_mode, hooks, config_fetcher, logger, config_cache, false) cache_policy.close - settings, _ = cache_policy.get_settings - expect(settings.fetch("testKey").fetch(VALUE)).to eq 1 + config, _ = cache_policy.get_config + settings = config.fetch(FEATURE_FLAGS) + expect(settings.fetch("testKey").fetch(VALUE).fetch(INT_VALUE)).to eq 1 sleep(2.2) - settings, _ = cache_policy.get_settings - expect(settings.fetch("testKey").fetch(VALUE)).to eq 1 + config, _ = cache_policy.get_config + settings = config.fetch(FEATURE_FLAGS) + expect(settings.fetch("testKey").fetch(VALUE).fetch(INT_VALUE)).to eq 1 cache_policy.close end @@ -189,8 +196,9 @@ cache_policy = ConfigService.new("", polling_mode, hooks, config_fetcher, logger, config_cache, false) # first call - settings, _ = cache_policy.get_settings - expect(settings.fetch("testStringKey").fetch(VALUE)).to eq "testValue" + config, _ = cache_policy.get_config + settings = config.fetch(FEATURE_FLAGS) + expect(settings.fetch("testStringKey").fetch(VALUE).fetch(STRING_VALUE)).to eq "testValue" WebMock.stub_request(:get, Regexp.new('https://.*')).to_return(status: 500, body: "", headers: {}) @@ -198,8 +206,9 @@ sleep(1.5) # previous value returned because of the refresh failure - settings, _ = cache_policy.get_settings - expect(settings.fetch("testStringKey").fetch(VALUE)).to eq "testValue" + config, _ = cache_policy.get_config + settings = config.fetch(FEATURE_FLAGS) + expect(settings.fetch("testStringKey").fetch(VALUE).fetch(STRING_VALUE)).to eq "testValue" cache_policy.close end @@ -221,22 +230,24 @@ hooks = Hooks.new logger = ConfigCatLogger.new(hooks) cache_policy = ConfigService.new("", polling_mode, hooks, config_fetcher, logger, config_cache, false) - settings, _ = cache_policy.get_settings + config, _ = cache_policy.get_config + settings = config.fetch(FEATURE_FLAGS) end_time = Time.now.utc elapsed_time = end_time - start_time # max init wait time should be ignored when cache is not expired expect(elapsed_time).to be <= max_init_wait_time_seconds - expect(settings.fetch("testKey").fetch(VALUE)).to eq "testValue" + expect(settings.fetch("testKey").fetch(VALUE).fetch(STRING_VALUE)).to eq "testValue" expect(config_fetcher.get_call_count).to eq 0 expect(config_fetcher.get_fetch_count).to eq 0 sleep(3) - settings, _ = cache_policy.get_settings + config, _ = cache_policy.get_config + settings = config.fetch(FEATURE_FLAGS) - expect(settings.fetch("testKey").fetch(VALUE)).to eq "testValue" + expect(settings.fetch("testKey").fetch(VALUE).fetch(STRING_VALUE)).to eq "testValue" expect(config_fetcher.get_call_count).to eq 1 expect(config_fetcher.get_fetch_count).to eq 1 @@ -260,9 +271,10 @@ logger = ConfigCatLogger.new(hooks) cache_policy = ConfigService.new("", polling_mode, hooks, config_fetcher, logger, config_cache, false) - settings, _ = cache_policy.get_settings + config, _ = cache_policy.get_config + settings = config.fetch(FEATURE_FLAGS) - expect(settings.fetch("testKey").fetch(VALUE)).to eq "testValue" + expect(settings.fetch("testKey").fetch(VALUE).fetch(STRING_VALUE)).to eq "testValue" expect(config_fetcher.get_call_count).to eq 1 expect(config_fetcher.get_fetch_count).to eq 1 @@ -286,14 +298,15 @@ hooks = Hooks.new logger = ConfigCatLogger.new(hooks) cache_policy = ConfigService.new("", polling_mode, hooks, config_fetcher, logger, config_cache, false) - settings, _ = cache_policy.get_settings + config, _ = cache_policy.get_config + settings = config.fetch(FEATURE_FLAGS) end_time = Time.now.utc elapsed_time = end_time - start_time expect(elapsed_time).to be > max_init_wait_time_seconds expect(elapsed_time).to be < max_init_wait_time_seconds + 1 - expect(settings.fetch("testKey").fetch(VALUE)).to eq "testValue" - expect(settings.fetch("testKey2").fetch(VALUE)).to eq "testValue2" + expect(settings.fetch("testKey").fetch(VALUE).fetch(STRING_VALUE)).to eq "testValue" + expect(settings.fetch("testKey2").fetch(VALUE).fetch(STRING_VALUE)).to eq "testValue2" cache_policy.close end @@ -314,8 +327,9 @@ cache_policy.set_offline expect(cache_policy.offline?).to be true - settings, _ = cache_policy.get_settings - expect(settings.fetch("testStringKey").fetch(VALUE)).to eq "testValue" + config, _ = cache_policy.get_config + settings = config.fetch(FEATURE_FLAGS) + expect(settings.fetch("testStringKey").fetch(VALUE).fetch(STRING_VALUE)).to eq "testValue" expect(stub_request).to have_been_made.times(2) sleep(2) @@ -341,14 +355,14 @@ cache_policy = ConfigService.new("", polling_mode, hooks, config_fetcher, logger, config_cache, true) expect(cache_policy.offline?).to be true - settings, _ = cache_policy.get_settings - expect(settings).to be nil + config, _ = cache_policy.get_config + expect(config).to be nil expect(stub_request).to have_been_made.times(0) sleep(2) - settings, _ = cache_policy.get_settings - expect(settings).to be nil + config, _ = cache_policy.get_config + expect(config).to be nil expect(stub_request).to have_been_made.times(0) cache_policy.set_online @@ -356,8 +370,9 @@ sleep(2.5) - settings, _ = cache_policy.get_settings - expect(settings.fetch("testStringKey").fetch(VALUE)).to eq "testValue" + config, _ = cache_policy.get_config + settings = config.fetch(FEATURE_FLAGS) + expect(settings.fetch("testStringKey").fetch(VALUE).fetch(STRING_VALUE)).to eq "testValue" expect(stub_request).to have_been_made.at_least_twice cache_policy.close diff --git a/spec/configcat/configcache_spec.rb b/spec/configcat/configcache_spec.rb index b6991ae..f3cd52b 100644 --- a/spec/configcat/configcache_spec.rb +++ b/spec/configcat/configcache_spec.rb @@ -18,8 +18,8 @@ end it "test_cache_key" do - expect(ConfigService.send(:get_cache_key, 'test1')).to eq('147c5b4c2b2d7c77e1605b1a4309f0ea6684a0c6') - expect(ConfigService.send(:get_cache_key, 'test2')).to eq('c09513b1756de9e4bc48815ec7a142b2441ed4d5') + expect(ConfigService.send(:get_cache_key, 'configcat-sdk-1/TEST_KEY-0123456789012/1234567890123456789012')).to eq('f83ba5d45bceb4bb704410f51b704fb6dfa19942') + expect(ConfigService.send(:get_cache_key, 'configcat-sdk-1/TEST_KEY2-123456789012/1234567890123456789012')).to eq('da7bfd8662209c8ed3f9db96daed4f8d91ba5876') end it "test_cache_payload" do @@ -29,10 +29,10 @@ expect(entry.serialize).to eq('1686756435844' + "\n" + etag + "\n" + TEST_JSON) end - it "tests_invalid_cache_content" do + it "test_invalid_cache_content" do hook_callbacks = HookCallbacks.new hooks = Hooks.new(on_error: hook_callbacks.method(:on_error)) - config_json_string = TEST_JSON_FORMAT % { value: '"test"' } + config_json_string = TEST_JSON_FORMAT % { value_type: SettingType::STRING, value: '{"s": "test"}' } config_cache = SingleValueConfigCache.new(ConfigEntry.new( JSON.parse(config_json_string), 'test-etag', @@ -40,21 +40,21 @@ Utils.get_utc_now_seconds_since_epoch).serialize ) - client = ConfigCatClient.get('test', ConfigCatOptions.new(polling_mode: PollingMode.manual_poll, - config_cache: config_cache, - hooks: hooks)) + client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions.new(polling_mode: PollingMode.manual_poll, + config_cache: config_cache, + hooks: hooks)) expect(client.get_value('testKey', 'default')).to eq('test') expect(hook_callbacks.error_call_count).to eq(0) # Invalid fetch time in cache - config_cache.value = ['text', 'test-etag', TEST_JSON_FORMAT % { value: '"test2"' }].join("\n") + config_cache.value = ['text', 'test-etag', TEST_JSON_FORMAT % { value_type: SettingType::STRING, value: '{"s": "test2}"' }].join("\n") expect(client.get_value('testKey', 'default')).to eq('test') expect(hook_callbacks.error).to include('Error occurred while reading the cache. Invalid fetch time: text') # Number of values is fewer than expected - config_cache.value = [Utils.get_utc_now_seconds_since_epoch.to_s, TEST_JSON_FORMAT % { value: '"test2"' }].join("\n") + config_cache.value = [Utils.get_utc_now_seconds_since_epoch.to_s, TEST_JSON_FORMAT % { value_type: SettingType::STRING, value: '{"s": "test2}"' }].join("\n") expect(client.get_value('testKey', 'default')).to eq('test') expect(hook_callbacks.error).to include('Error occurred while reading the cache. Number of values is fewer than expected.') diff --git a/spec/configcat/configcatclient_spec.rb b/spec/configcat/configcatclient_spec.rb index f32bc74..4957906 100644 --- a/spec/configcat/configcatclient_spec.rb +++ b/spec/configcat/configcatclient_spec.rb @@ -4,12 +4,12 @@ RSpec.describe ConfigCat::ConfigCatClient do it "test_ensure_singleton_per_sdk_key" do - client1 = ConfigCatClient.get('test', ConfigCatOptions.new(polling_mode: PollingMode.manual_poll)) - client2 = ConfigCatClient.get('test', ConfigCatOptions.new(polling_mode: PollingMode.manual_poll)) + client1 = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions.new(polling_mode: PollingMode.manual_poll)) + client2 = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions.new(polling_mode: PollingMode.manual_poll)) expect(client1).to eq(client2) ConfigCatClient.close_all - client1 = ConfigCatClient.get('test', ConfigCatOptions.new(polling_mode: PollingMode.manual_poll)) + client1 = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions.new(polling_mode: PollingMode.manual_poll)) expect(client1).not_to eq(client2) ConfigCatClient.close_all @@ -21,58 +21,126 @@ }.to raise_error(ConfigCatClientException) end + [ + ["sdk-key-90123456789012", false, false], + ["sdk-key-9012345678901/1234567890123456789012", false, false], + ["sdk-key-90123456789012/123456789012345678901", false, false], + ["sdk-key-90123456789012/12345678901234567890123", false, false], + ["sdk-key-901234567890123/1234567890123456789012", false, false], + ["sdk-key-90123456789012/1234567890123456789012", false, true], + ["configcat-sdk-1/sdk-key-90123456789012", false, false], + ["configcat-sdk-1/sdk-key-9012345678901/1234567890123456789012", false, false], + ["configcat-sdk-1/sdk-key-90123456789012/123456789012345678901", false, false], + ["configcat-sdk-1/sdk-key-90123456789012/12345678901234567890123", false, false], + ["configcat-sdk-1/sdk-key-901234567890123/1234567890123456789012", false, false], + ["configcat-sdk-1/sdk-key-90123456789012/1234567890123456789012", false, true], + ["configcat-sdk-2/sdk-key-90123456789012/1234567890123456789012", false, false], + ["configcat-proxy/", false, false], + ["configcat-proxy/", true, false], + ["configcat-proxy/sdk-key-90123456789012", false, false], + ["configcat-proxy/sdk-key-90123456789012", true, true], + ].each do |sdk_key, custom_base_url, is_valid| + it "test_sdk_key_format_validation (#{sdk_key}, #{custom_base_url}, #{is_valid})" do + if custom_base_url + base_url = 'https://my-configcat-proxy' + else + base_url = nil + end + + begin + ConfigCatClient.get(sdk_key, ConfigCatOptions.new(base_url: base_url)) + expect(is_valid).to eq(true) + rescue ConfigCatClientException => e + expect(is_valid).to eq(false) + end + end + end + it "test_bool" do - client = ConfigCatClient.get("test", ConfigCatOptions.new(polling_mode: PollingMode.manual_poll, - config_cache: ConfigCacheMock.new)) + client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions.new(polling_mode: PollingMode.manual_poll, + config_cache: ConfigCacheMock.new)) expect(client.get_value("testBoolKey", false)).to eq true client.close() end it "test_string" do - client = ConfigCatClient.get("test", ConfigCatOptions.new(polling_mode: PollingMode.manual_poll, - config_cache: ConfigCacheMock.new)) + client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions.new(polling_mode: PollingMode.manual_poll, + config_cache: ConfigCacheMock.new)) expect(client.get_value("testStringKey", "default")).to eq "testValue" client.close() end it "test_int" do - client = ConfigCatClient.get("test", ConfigCatOptions.new(polling_mode: PollingMode.manual_poll, - config_cache: ConfigCacheMock.new)) + client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions.new(polling_mode: PollingMode.manual_poll, + config_cache: ConfigCacheMock.new)) expect(client.get_value("testIntKey", 0)).to eq 1 client.close() end it "test_double" do - client = ConfigCatClient.get("test", ConfigCatOptions.new(polling_mode: PollingMode.manual_poll, - config_cache: ConfigCacheMock.new)) + client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions.new(polling_mode: PollingMode.manual_poll, + config_cache: ConfigCacheMock.new)) expect(client.get_value("testDoubleKey", 0.0)).to eq 1.1 client.close() end it "test_unknown" do - client = ConfigCatClient.get("test", ConfigCatOptions.new(polling_mode: PollingMode.manual_poll, - config_cache: ConfigCacheMock.new)) + client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions.new(polling_mode: PollingMode.manual_poll, + config_cache: ConfigCacheMock.new)) expect(client.get_value("testUnknownKey", "default")).to eq "default" client.close() end it "test_invalidation" do - client = ConfigCatClient.get("test", ConfigCatOptions.new(polling_mode: PollingMode.manual_poll, - config_cache: ConfigCacheMock.new)) + client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions.new(polling_mode: PollingMode.manual_poll, + config_cache: ConfigCacheMock.new)) expect(client.get_value("testBoolKey", false)).to eq true client.close() end + it "test_incorrect_json" do + config_json_string = '{ + "f": { + "testKey": { + "t": 0, + "r": [ { + "c": [ { "u": { "a": "Custom1", "c": 19, "d": "wrong_utc_timestamp" } } ], + "s": { "v": { "b": true } } + } ], + "v": { "b": false } + } + } + }' + config_cache = SingleValueConfigCache.new(ConfigEntry.new( + JSON.parse(config_json_string), + 'test-etag', + config_json_string, + Utils.get_utc_now_seconds_since_epoch).serialize + ) + + + hook_callbacks = HookCallbacks.new + hooks = Hooks.new(on_error: hook_callbacks.method(:on_error)) + client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions.new(polling_mode: PollingMode.manual_poll, + config_cache: config_cache, + hooks: hooks)) + + expect(client.get_value('testKey', false, User.new('1234', custom: { 'Custom1' => 1681118000.56 }))).to eq(false) + expect(hook_callbacks.error_call_count).to eq(1) + expect(hook_callbacks.error).to include("Failed to evaluate setting 'testKey'.") + client.close + end + it "test_get_all_keys" do - client = ConfigCatClient.get("test", ConfigCatOptions.new(polling_mode: PollingMode.manual_poll, - config_cache: ConfigCacheMock.new)) + client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions.new(polling_mode: PollingMode.manual_poll, + config_cache: ConfigCacheMock.new)) expect(Set.new(client.get_all_keys())).to eq Set.new(["testBoolKey", "testStringKey", "testIntKey", "testDoubleKey", "key1", "key2"]) client.close() end it "test_get_all_values" do - client = ConfigCatClient.get("test", ConfigCatOptions.new(polling_mode: PollingMode.manual_poll, - config_cache: ConfigCacheMock.new)) + client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions.new(polling_mode: PollingMode.manual_poll, + config_cache: ConfigCacheMock.new)) all_values = client.get_all_values() expect(all_values.size).to eq 6 expect(all_values["testBoolKey"]).to eq true @@ -80,13 +148,13 @@ expect(all_values["testIntKey"]).to eq 1 expect(all_values["testDoubleKey"]).to eq 1.1 expect(all_values["key1"]).to eq true - expect(all_values["key2"]).to eq false + expect(all_values["key2"]).to eq "fake4" client.close() end it "test_get_all_value_details" do - client = ConfigCatClient.get('test', ConfigCatOptions.new(polling_mode: PollingMode.manual_poll, - config_cache: ConfigCacheMock.new)) + client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions.new(polling_mode: PollingMode.manual_poll, + config_cache: ConfigCacheMock.new)) all_details = client.get_all_value_details def details_by_key(all_details, key) @@ -118,12 +186,12 @@ def details_by_key(all_details, key) details = details_by_key(all_details, 'key1') expect(details.key).to eq 'key1' expect(details.value).to eq true - expect(details.variation_id).to eq 'fakeId1' + expect(details.variation_id).to eq 'id3' details = details_by_key(all_details, 'key2') expect(details.key).to eq 'key2' - expect(details.value).to eq false - expect(details.variation_id).to eq 'fakeId2' + expect(details.value).to eq 'fake4' + expect(details.variation_id).to eq 'id4' client.close end @@ -131,7 +199,7 @@ def details_by_key(all_details, key) it "test_get_value_details" do WebMock.stub_request(:get, Regexp.new('https://.*')).to_return(status: 200, body: TEST_OBJECT_JSON, headers: {}) - client = ConfigCatClient.get('test', ConfigCatOptions.new(polling_mode: PollingMode.manual_poll)) + client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions.new(polling_mode: PollingMode.manual_poll)) client.force_refresh user = User.new("test@test1.com") @@ -142,11 +210,8 @@ def details_by_key(all_details, key) expect(details.variation_id).to eq('id1') expect(details.is_default_value).to be_falsey expect(details.error).to be_nil - expect(details.matched_evaluation_percentage_rule).to be_nil - expect(details.matched_evaluation_rule[VALUE]).to eq('fake1') - expect(details.matched_evaluation_rule[COMPARATOR]).to eq(2) - expect(details.matched_evaluation_rule[COMPARISON_ATTRIBUTE]).to eq('Identifier') - expect(details.matched_evaluation_rule[COMPARISON_VALUE]).to eq('@test1.com') + expect(details.matched_percentage_option).to be_nil + expect(details.matched_targeting_rule[SERVED_VALUE][VALUE][STRING_VALUE]).to eq('fake1') expect(details.user.to_s).to eq(user.to_s) now = Utils.get_utc_now_seconds_since_epoch expect(now).to be >= details.fetch_time.to_f @@ -156,8 +221,8 @@ def details_by_key(all_details, key) end it "test_default_user_get_value" do - client = ConfigCatClient.get('test', ConfigCatOptions.new(polling_mode: PollingMode.manual_poll, - config_cache: ConfigCacheMock.new)) + client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions.new(polling_mode: PollingMode.manual_poll, + config_cache: ConfigCacheMock.new)) user1 = User.new("test@test1.com") user2 = User.new("test@test2.com") @@ -172,8 +237,8 @@ def details_by_key(all_details, key) end it "test_default_user_get_all_values" do - client = ConfigCatClient.get('test', ConfigCatOptions.new(polling_mode: PollingMode.manual_poll, - config_cache: ConfigCacheMock.new)) + client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions.new(polling_mode: PollingMode.manual_poll, + config_cache: ConfigCacheMock.new)) user1 = User.new("test@test1.com") user2 = User.new("test@test2.com") @@ -186,7 +251,7 @@ def details_by_key(all_details, key) expect(all_values['testIntKey']).to eq(1) expect(all_values['testDoubleKey']).to eq(1.1) expect(all_values['key1']).to be(true) - expect(all_values['key2']).to be(false) + expect(all_values['key2']).to eq('fake6') all_values = client.get_all_values(user2) # Two dictionary should have exactly the same elements, order doesn't matter. @@ -196,7 +261,7 @@ def details_by_key(all_details, key) expect(all_values['testIntKey']).to eq(1) expect(all_values['testDoubleKey']).to eq(1.1) expect(all_values['key1']).to be(true) - expect(all_values['key2']).to be(false) + expect(all_values['key2']).to eq('fake8') client.clear_default_user() all_values = client.get_all_values @@ -206,7 +271,7 @@ def details_by_key(all_details, key) expect(all_values['testIntKey']).to eq(1) expect(all_values['testDoubleKey']).to eq(1.1) expect(all_values['key1']).to be(true) - expect(all_values['key2']).to be(false) + expect(all_values['key2']).to eq('fake4') client.close end @@ -214,7 +279,7 @@ def details_by_key(all_details, key) it "test_online_offline" do stub_request = WebMock.stub_request(:get, Regexp.new('https://.*')).to_return(status: 200, body: TEST_OBJECT_JSON, headers: {}) - client = ConfigCatClient.get('test', ConfigCatOptions.new(polling_mode: PollingMode.manual_poll)) + client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions.new(polling_mode: PollingMode.manual_poll)) expect(client.offline?).to be false @@ -242,8 +307,8 @@ def details_by_key(all_details, key) it "test_init_offline" do stub_request = WebMock.stub_request(:get, Regexp.new('https://.*')).to_return(status: 200, body: TEST_OBJECT_JSON, headers: {}) - client = ConfigCatClient.get('test', ConfigCatOptions.new(polling_mode: PollingMode.manual_poll, - offline: true)) + client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions.new(polling_mode: PollingMode.manual_poll, + offline: true)) expect(client.offline?).to be true @@ -261,37 +326,114 @@ def details_by_key(all_details, key) client.close end + [ + # no type mismatch warning + ['testStringKey', 'test@example.com', 'default', 'testValue', false], + ['testBoolKey', nil, false, true, false], + ['testBoolKey', nil, nil, true, false], + ['testIntKey', nil, 3.14, 1, false], + ['testIntKey', nil, 42, 1, false], + ['testDoubleKey', nil, 3.14, 1.1, false], + ['testDoubleKey', nil, 42, 1.1, false], + # Type mismatch warning test cases + ['testStringKey', 'test@example.com', 0, 'testValue', true], + ['testStringKey', 'test@example.com', false, 'testValue', true], + ['testBoolKey', nil, 0, true, true], + ['testBoolKey', nil, 0.1, true, true], + ['testBoolKey', nil, 'default', true, true] + ].each do |key, user_id, default_value, expected_value, is_warning| + it "test_default_value_and_setting_type_mismatch (#{key}, #{user_id}, #{default_value}, #{expected_value}, #{is_warning})" do + stub_request = WebMock.stub_request(:get, Regexp.new('https://.*')).to_return(status: 200, body: TEST_OBJECT_JSON, headers: {}) + + begin + # Setup logging + logger = ConfigCat.logger + log_stream = StringIO.new + ConfigCat.logger = Logger.new(log_stream, level: Logger::WARN) + + client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions.new(polling_mode: PollingMode.manual_poll)) + client.force_refresh + + user = user_id ? User.new(user_id) : nil + value = client.get_value(key, default_value, user) + expect(value).to eq(expected_value) + + log_stream.rewind + log = log_stream.read + + if is_warning + expect(log).to include("[4002] The type of a setting does not match the type of the specified default value (#{default_value}). " \ + "Setting's type was #{expected_value.class} but the default value's type was #{default_value.class}. " \ + "Please make sure that using a default value not matching the setting's type was intended.") + else + expect(log).to be_empty + end + ensure + client.close + ConfigCat.logger = logger + end + end + end + + # variation id tests + it "test_get_variation_id" do - client = ConfigCatClient.get("test", ConfigCatOptions.new(polling_mode: PollingMode.manual_poll, - config_cache: ConfigCacheMock.new)) - expect(client.get_value_details("key1", nil).variation_id).to eq "fakeId1" - expect(client.get_value_details("key2", nil).variation_id).to eq "fakeId2" + client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions.new(polling_mode: PollingMode.manual_poll, + config_cache: ConfigCacheMock.new)) + expect(client.get_value_details("key1", nil).variation_id).to eq "id3" + expect(client.get_value_details("key2", nil).variation_id).to eq "id4" client.close() end it "test_get_variation_id_not_found" do - client = ConfigCatClient.get("test", ConfigCatOptions.new(polling_mode: PollingMode.manual_poll, - config_cache: ConfigCacheMock.new)) + client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions.new(polling_mode: PollingMode.manual_poll, + config_cache: ConfigCacheMock.new)) expect(client.get_value_details("nonexisting", "default_value").variation_id).to be_nil client.close() end it "test_get_variation_id_empty_config" do - client = ConfigCatClient.get("test", ConfigCatOptions.new(polling_mode: PollingMode.manual_poll, - config_cache: ConfigCacheMock.new)) + client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions.new(polling_mode: PollingMode.manual_poll, + config_cache: ConfigCacheMock.new)) expect(client.get_value_details("nonexisting", "default_value").variation_id).to be_nil client.close() end it "test_get_key_and_value" do - client = ConfigCatClient.get("test", ConfigCatOptions.new(polling_mode: PollingMode.manual_poll, - config_cache: ConfigCacheMock.new)) - result = client.get_key_and_value("fakeId1") + client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions.new(polling_mode: PollingMode.manual_poll, + config_cache: ConfigCacheMock.new)) + result = client.get_key_and_value("id1") + expect(result.key).to eq "testStringKey" + expect(result.value).to eq "fake1" + + result = client.get_key_and_value("id2") + expect(result.key).to eq "testStringKey" + expect(result.value).to eq "fake2" + + result = client.get_key_and_value("id3") expect(result.key).to eq "key1" expect(result.value).to eq true - result = client.get_key_and_value("fakeId2") + + result = client.get_key_and_value("id4") expect(result.key).to eq "key2" - expect(result.value).to eq false + expect(result.value).to eq "fake4" + + result = client.get_key_and_value("id5") + expect(result.key).to eq "key2" + expect(result.value).to eq "fake5" + + result = client.get_key_and_value("id6") + expect(result.key).to eq "key2" + expect(result.value).to eq "fake6" + + result = client.get_key_and_value("id7") + expect(result.key).to eq "key2" + expect(result.value).to eq "fake7" + + result = client.get_key_and_value("id8") + expect(result.key).to eq "key2" + expect(result.value).to eq "fake8" + client.close() end end diff --git a/spec/configcat/configfetcher_spec.rb b/spec/configcat/configfetcher_spec.rb index 23be318..6a5b8d7 100644 --- a/spec/configcat/configfetcher_spec.rb +++ b/spec/configcat/configfetcher_spec.rb @@ -4,6 +4,7 @@ RSpec.describe ConfigCat::ConfigFetcher do it "test_simple_fetch_success" do + test_json = '{"test": "json"}' uri_template = Addressable::Template.new "https://{base_url}/{base_path}/{api_key}/{base_ext}" WebMock.stub_request(:get, uri_template) .with( @@ -14,17 +15,19 @@ 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3' } ) - .to_return(status: 200, body: TEST_JSON, headers: {}) + .to_return(status: 200, body: test_json, headers: {}) log = ConfigCatLogger.new(Hooks.new) fetcher = ConfigCat::ConfigFetcher.new("", log, "m") fetch_response = fetcher.get_configuration() expect(fetch_response.is_fetched()).to be true - expect(fetch_response.entry.config).to eq JSON.parse(TEST_JSON) + expect(fetch_response.entry.config).to eq JSON.parse(test_json) + expect(fetch_response.entry.config_json_string).to eq test_json end it "test_fetch_not_modified_etag" do etag = "test" + test_json = '{"test": "json"}' uri_template = Addressable::Template.new "https://{base_url}/{base_path}/{api_key}/{base_ext}" WebMock.stub_request(:get, uri_template) .with( @@ -35,12 +38,13 @@ 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3' } ) - .to_return(status: 200, body: TEST_JSON, headers: { "ETag" => etag }) + .to_return(status: 200, body: test_json, headers: { "ETag" => etag }) log = ConfigCatLogger.new(Hooks.new) fetcher = ConfigCat::ConfigFetcher.new("", log, "m") fetch_response = fetcher.get_configuration() expect(fetch_response.is_fetched()).to be true - expect(fetch_response.entry.config).to eq JSON.parse(TEST_JSON) + expect(fetch_response.entry.config).to eq JSON.parse(test_json) + expect(fetch_response.entry.config_json_string).to eq test_json expect(fetch_response.entry.etag).to eq etag WebMock.stub_request(:get, uri_template) diff --git a/spec/configcat/hooks_spec.rb b/spec/configcat/hooks_spec.rb index 014821f..6f22ea1 100644 --- a/spec/configcat/hooks_spec.rb +++ b/spec/configcat/hooks_spec.rb @@ -14,16 +14,18 @@ ) config_cache = ConfigCacheMock.new - client = ConfigCatClient.get('test', ConfigCatOptions.new(polling_mode: PollingMode.manual_poll, - config_cache: config_cache, - hooks: hooks)) + client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions.new(polling_mode: PollingMode.manual_poll, + config_cache: config_cache, + hooks: hooks)) value = client.get_value('testStringKey', '') expect(value).to eq('testValue') expect(hook_callbacks.is_ready).to be true expect(hook_callbacks.is_ready_call_count).to eq(1) - expect(hook_callbacks.changed_config).to eq(TEST_OBJECT.fetch(FEATURE_FLAGS)) + extended_config = TEST_OBJECT + Config.fixup_config_salt_and_segments(extended_config) + expect(hook_callbacks.changed_config).to eq(extended_config.fetch(FEATURE_FLAGS)) expect(hook_callbacks.changed_config_call_count).to eq(1) expect(hook_callbacks.evaluation_details).not_to be nil expect(hook_callbacks.evaluation_details_call_count).to eq(1) @@ -42,9 +44,9 @@ hooks.add_on_error(hook_callbacks.method(:on_error)) config_cache = ConfigCacheMock.new - client = ConfigCatClient.get('test', ConfigCatOptions.new(polling_mode: PollingMode.manual_poll, - config_cache: config_cache, - hooks: hooks)) + client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions.new(polling_mode: PollingMode.manual_poll, + config_cache: config_cache, + hooks: hooks)) value = client.get_value('testStringKey', '') @@ -65,7 +67,7 @@ WebMock.stub_request(:get, Regexp.new('https://.*')).to_return(status: 200, body: TEST_OBJECT_JSON, headers: {}) hook_callbacks = HookCallbacks.new - client = ConfigCatClient.get('test', ConfigCatOptions.new(polling_mode: PollingMode.manual_poll)) + client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions.new(polling_mode: PollingMode.manual_poll)) client.hooks.add_on_flag_evaluated(hook_callbacks.method(:on_flag_evaluated)) client.force_refresh @@ -80,11 +82,8 @@ expect(details.variation_id).to eq("id1") expect(details.is_default_value).to be false expect(details.error).to be nil - expect(details.matched_evaluation_percentage_rule).to be nil - expect(details.matched_evaluation_rule[VALUE]).to eq("fake1") - expect(details.matched_evaluation_rule[COMPARATOR]).to eq(2) - expect(details.matched_evaluation_rule[COMPARISON_ATTRIBUTE]).to eq("Identifier") - expect(details.matched_evaluation_rule[COMPARISON_VALUE]).to eq("@test1.com") + expect(details.matched_percentage_option).to be nil + expect(details.matched_targeting_rule[SERVED_VALUE][VALUE][STRING_VALUE]).to eq("fake1") expect(details.user.to_s).to eq(user.to_s) now = Utils.get_utc_now_seconds_since_epoch expect(details.fetch_time.to_f).to be <= now @@ -103,7 +102,7 @@ on_flag_evaluated: hook_callbacks.method(:callback_exception), on_error: hook_callbacks.method(:callback_exception) ) - client = ConfigCatClient.get('test', ConfigCatOptions.new(polling_mode: PollingMode.manual_poll, hooks: hooks)) + client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions.new(polling_mode: PollingMode.manual_poll, hooks: hooks)) client.force_refresh diff --git a/spec/configcat/lazyloadingcachepolicy_spec.rb b/spec/configcat/lazyloadingcachepolicy_spec.rb index a1fe0a5..ef25fac 100644 --- a/spec/configcat/lazyloadingcachepolicy_spec.rb +++ b/spec/configcat/lazyloadingcachepolicy_spec.rb @@ -11,8 +11,9 @@ hooks = Hooks.new logger = ConfigCatLogger.new(hooks) cache_policy = ConfigService.new("", polling_mode, hooks, config_fetcher, logger, config_cache, false) - settings, _ = cache_policy.get_settings - expect(settings.fetch("testKey").fetch(VALUE)).to eq "testValue" + config, _ = cache_policy.get_config + settings = config.fetch(FEATURE_FLAGS) + expect(settings.fetch("testKey").fetch(VALUE).fetch(STRING_VALUE)).to eq "testValue" cache_policy.close end @@ -25,19 +26,22 @@ cache_policy = ConfigService.new("", polling_mode, hooks, config_fetcher, logger, config_cache, false) # Get value from Config Store, which indicates a config_fetcher call - settings, _ = cache_policy.get_settings - expect(settings.fetch("testKey").fetch(VALUE)).to eq "testValue" + config, _ = cache_policy.get_config + settings = config.fetch(FEATURE_FLAGS) + expect(settings.fetch("testKey").fetch(VALUE).fetch(STRING_VALUE)).to eq "testValue" expect(config_fetcher.get_call_count).to eq 1 # Get value from Config Store, which doesn't indicate a config_fetcher call (cache) - settings, _ = cache_policy.get_settings - expect(settings.fetch("testKey").fetch(VALUE)).to eq "testValue" + config, _ = cache_policy.get_config + settings = config.fetch(FEATURE_FLAGS) + expect(settings.fetch("testKey").fetch(VALUE).fetch(STRING_VALUE)).to eq "testValue" expect(config_fetcher.get_call_count).to eq 1 # Get value from Config Store, which indicates a config_fetcher call - 1 sec cache TTL sleep(1) - settings, _ = cache_policy.get_settings - expect(settings.fetch("testKey").fetch(VALUE)).to eq "testValue" + config, _ = cache_policy.get_config + settings = config.fetch(FEATURE_FLAGS) + expect(settings.fetch("testKey").fetch(VALUE).fetch(STRING_VALUE)).to eq "testValue" expect(config_fetcher.get_call_count).to eq 2 cache_policy.close @@ -52,17 +56,19 @@ cache_policy = ConfigService.new("", polling_mode, hooks, config_fetcher, logger, config_cache, false) # Get value from Config Store, which indicates a config_fetcher call - settings, fetch_time = cache_policy.get_settings - expect(settings.fetch("testKey").fetch(VALUE)).to eq "testValue" + config, fetch_time = cache_policy.get_config + settings = config.fetch(FEATURE_FLAGS) + expect(settings.fetch("testKey").fetch(VALUE).fetch(STRING_VALUE)).to eq "testValue" expect(config_fetcher.get_call_count).to eq 1 - # assume 160 seconds has elapsed since the last call enough to do a + # assume 160 seconds has elapsed since the last call enough to do a force refresh allow(Time).to receive(:now).and_return(Time.at(fetch_time + 161)) # Get value from Config Store, which indicates a config_fetcher call after cache invalidation cache_policy.refresh - settings, _ = cache_policy.get_settings - expect(settings.fetch("testKey").fetch(VALUE)).to eq "testValue" + config, _ = cache_policy.get_config + settings = config.fetch(FEATURE_FLAGS) + expect(settings.fetch("testKey").fetch(VALUE).fetch(STRING_VALUE)).to eq "testValue" expect(config_fetcher.get_call_count).to eq 2 cache_policy.close @@ -84,7 +90,7 @@ successful_fetch_response.entry.fetch_time = now.utc.to_f # Get value from Config Store, which indicates a config_fetcher call - cache_policy.get_settings + cache_policy.get_config expect(config_fetcher).to have_received(:get_configuration).once # when the cache timeout is still within the limit skip any network @@ -92,11 +98,11 @@ # to acquire the lock at the same time, but only really one needs to update successful_fetch_response.entry.fetch_time = now.utc.to_f - 159 - cache_policy.get_settings + cache_policy.get_config expect(config_fetcher).to have_received(:get_configuration).once successful_fetch_response.entry.fetch_time = now.utc.to_f - 161 - cache_policy.get_settings + cache_policy.get_config expect(config_fetcher).to have_received(:get_configuration).twice end @@ -109,8 +115,8 @@ cache_policy = ConfigService.new("", polling_mode, hooks, config_fetcher, logger, config_cache, false) # Get value from Config Store, which indicates a config_fetcher call - settings, _ = cache_policy.get_settings - expect(settings).to be nil + config, _ = cache_policy.get_config + expect(config).to be nil cache_policy.close end @@ -128,17 +134,19 @@ logger = ConfigCatLogger.new(hooks) cache_policy = ConfigService.new("", polling_mode, hooks, config_fetcher, logger, config_cache, false) - settings, _ = cache_policy.get_settings + config, _ = cache_policy.get_config + settings = config.fetch(FEATURE_FLAGS) - expect(settings.fetch("testKey").fetch(VALUE)).to eq "testValue" + expect(settings.fetch("testKey").fetch(VALUE).fetch(STRING_VALUE)).to eq "testValue" expect(config_fetcher.get_call_count).to eq 0 expect(config_fetcher.get_fetch_count).to eq 0 sleep(1) - settings, _ = cache_policy.get_settings + config, _ = cache_policy.get_config + settings = config.fetch(FEATURE_FLAGS) - expect(settings.fetch("testKey").fetch(VALUE)).to eq "testValue" + expect(settings.fetch("testKey").fetch(VALUE).fetch(STRING_VALUE)).to eq "testValue" expect(config_fetcher.get_call_count).to eq 1 expect(config_fetcher.get_fetch_count).to eq 1 @@ -160,15 +168,57 @@ logger = ConfigCatLogger.new(hooks) cache_policy = ConfigService.new("", polling_mode, hooks, config_fetcher, logger, config_cache, false) - settings, _ = cache_policy.get_settings + config, _ = cache_policy.get_config + settings = config.fetch(FEATURE_FLAGS) - expect(settings.fetch("testKey").fetch(VALUE)).to eq "testValue" + expect(settings.fetch("testKey").fetch(VALUE).fetch(STRING_VALUE)).to eq "testValue" expect(config_fetcher.get_call_count).to eq 1 expect(config_fetcher.get_fetch_count).to eq 1 cache_policy.close end + it "test_cache_TTL_respects_external_cache" do + WebMock.stub_request(:get, Regexp.new('https://.*')) + .to_return(status: 200, body: TEST_JSON_FORMAT % { value_type: SettingType::STRING, value: '{"s": "test-remote"}' }, + headers: {}) + + config_json_string_local = TEST_JSON_FORMAT % { value_type: SettingType::STRING, value: '{"s": "test-local"}' } + config_cache = SingleValueConfigCache.new(ConfigEntry.new( + JSON.parse(config_json_string_local), + 'etag', + config_json_string_local, + Utils.get_utc_now_seconds_since_epoch).serialize + ) + + polling_mode = PollingMode.lazy_load(cache_refresh_interval_seconds: 1) + hooks = Hooks.new + logger = ConfigCatLogger.new(hooks) + config_fetcher = ConfigFetcherMock.new + cache_policy = ConfigService.new("", polling_mode, hooks, config_fetcher, logger, config_cache, false) + + config, _ = cache_policy.get_config + settings = config.fetch(FEATURE_FLAGS) + + expect(settings.fetch("testKey").fetch(VALUE).fetch(STRING_VALUE)).to eq "test-local" + expect(config_fetcher.get_fetch_count).to eq 0 + + sleep(1) + + config_json_string_local = TEST_JSON_FORMAT % { value_type: SettingType::STRING, value: '{"s": "test-local2"}' } + config_cache.value = ConfigEntry.new( + JSON.parse(config_json_string_local), + 'etag2', + config_json_string_local, + Utils.get_utc_now_seconds_since_epoch).serialize + + config, _ = cache_policy.get_config + settings = config.fetch(FEATURE_FLAGS) + + expect(settings.fetch("testKey").fetch(VALUE).fetch(STRING_VALUE)).to eq "test-local2" + expect(config_fetcher.get_fetch_count).to eq 0 + end + it "test_online_offline" do stub_request = WebMock.stub_request(:get, Regexp.new('https://.*')).to_return(status: 200, body: TEST_OBJECT_JSON, headers: {}) @@ -180,8 +230,9 @@ cache_policy = ConfigService.new("", polling_mode, hooks, config_fetcher, logger, config_cache, false) expect(cache_policy.offline?).to be false - settings, _ = cache_policy.get_settings - expect(settings.fetch("testStringKey").fetch(VALUE)).to eq "testValue" + config, _ = cache_policy.get_config + settings = config.fetch(FEATURE_FLAGS) + expect(settings.fetch("testStringKey").fetch(VALUE).fetch(STRING_VALUE)).to eq "testValue" expect(stub_request).to have_been_made.times(1) cache_policy.set_offline @@ -189,15 +240,17 @@ sleep(1.5) - settings, _ = cache_policy.get_settings - expect(settings.fetch("testStringKey").fetch(VALUE)).to eq "testValue" + config, _ = cache_policy.get_config + settings = config.fetch(FEATURE_FLAGS) + expect(settings.fetch("testStringKey").fetch(VALUE).fetch(STRING_VALUE)).to eq "testValue" expect(stub_request).to have_been_made.times(1) cache_policy.set_online expect(cache_policy.offline?).to be false - settings, _ = cache_policy.get_settings - expect(settings.fetch("testStringKey").fetch(VALUE)).to eq "testValue" + config, _ = cache_policy.get_config + settings = config.fetch(FEATURE_FLAGS) + expect(settings.fetch("testStringKey").fetch(VALUE).fetch(STRING_VALUE)).to eq "testValue" expect(stub_request).to have_been_made.times(2) cache_policy.close @@ -214,21 +267,22 @@ cache_policy = ConfigService.new("", polling_mode, hooks, config_fetcher, logger, config_cache, true) expect(cache_policy.offline?).to be true - settings, _ = cache_policy.get_settings - expect(settings).to be nil + config, _ = cache_policy.get_config + expect(config).to be nil expect(stub_request).to have_been_made.times(0) sleep(1.5) - settings, _ = cache_policy.get_settings - expect(settings).to be nil + config, _ = cache_policy.get_config + expect(config).to be nil expect(stub_request).to have_been_made.times(0) cache_policy.set_online expect(cache_policy.offline?).to be false - settings, _ = cache_policy.get_settings - expect(settings.fetch("testStringKey").fetch(VALUE)).to eq "testValue" + config, _ = cache_policy.get_config + settings = config.fetch(FEATURE_FLAGS) + expect(settings.fetch("testStringKey").fetch(VALUE).fetch(STRING_VALUE)).to eq "testValue" expect(stub_request).to have_been_made.times(1) cache_policy.close diff --git a/spec/configcat/manualpollingcachepolicy_spec.rb b/spec/configcat/manualpollingcachepolicy_spec.rb index cf06f32..e11feb8 100644 --- a/spec/configcat/manualpollingcachepolicy_spec.rb +++ b/spec/configcat/manualpollingcachepolicy_spec.rb @@ -13,8 +13,8 @@ hooks = Hooks.new logger = ConfigCatLogger.new(hooks) cache_policy = ConfigService.new("", PollingMode.manual_poll, hooks, config_fetcher, logger, config_cache, false) - settings, _ = cache_policy.get_settings - expect(settings).to be nil + config, _ = cache_policy.get_config + expect(config).to be nil expect(config_fetcher.get_call_count).to eq 0 cache_policy.close end @@ -26,8 +26,9 @@ logger = ConfigCatLogger.new(hooks) cache_policy = ConfigService.new("", PollingMode.manual_poll, hooks, config_fetcher, logger, config_cache, false) cache_policy.refresh - settings, _ = cache_policy.get_settings - expect(settings.fetch("testKey").fetch(VALUE)).to eq "testValue" + config, _ = cache_policy.get_config + settings = config.fetch(FEATURE_FLAGS) + expect(settings.fetch("testKey").fetch(VALUE).fetch(STRING_VALUE)).to eq "testValue" expect(config_fetcher.get_call_count).to eq 1 cache_policy.close end @@ -39,8 +40,8 @@ logger = ConfigCatLogger.new(hooks) cache_policy = ConfigService.new('', PollingMode.manual_poll, hooks, config_fetcher, logger, config_cache, false) cache_policy.refresh - settings, _ = cache_policy.get_settings - expect(settings).to be nil + config, _ = cache_policy.get_config + expect(config).to be nil cache_policy.close end @@ -55,21 +56,23 @@ cache_policy = ConfigService.new("", polling_mode, hooks, config_fetcher, logger, config_cache, false) cache_policy.refresh - settings, _ = cache_policy.get_settings - expect(settings.fetch("testStringKey").fetch(VALUE)).to eq "testValue" + config, _ = cache_policy.get_config + settings = config.fetch(FEATURE_FLAGS) + expect(settings.fetch("testStringKey").fetch(VALUE).fetch(STRING_VALUE)).to eq "testValue" WebMock.stub_request(:get, Regexp.new('https://.*')).to_return(status: 500, body: "", headers: {}) cache_policy.refresh - settings, _ = cache_policy.get_settings - expect(settings.fetch("testStringKey").fetch(VALUE)).to eq "testValue" + config, _ = cache_policy.get_config + settings = config.fetch(FEATURE_FLAGS) + expect(settings.fetch("testStringKey").fetch(VALUE).fetch(STRING_VALUE)).to eq "testValue" cache_policy.close end it "test_cache" do stub_request = WebMock.stub_request(:get, Regexp.new('https://.*')) - .to_return(status: 200, body: TEST_JSON_FORMAT % { value: '"test"' }, + .to_return(status: 200, body: TEST_JSON_FORMAT % { value_type: SettingType::STRING, value: '{"s": "test"}' }, headers: { 'ETag' => 'test-etag' }) polling_mode = PollingMode.manual_poll @@ -81,8 +84,9 @@ start_time_milliseconds = (Utils.get_utc_now_seconds_since_epoch * 1000).floor cache_policy.refresh - settings, _ = cache_policy.get_settings - expect(settings.fetch("testKey").fetch(VALUE)).to eq "test" + config, _ = cache_policy.get_config + settings = config.fetch(FEATURE_FLAGS) + expect(settings.fetch("testKey").fetch(VALUE).fetch(STRING_VALUE)).to eq "test" expect(stub_request).to have_been_made.times(1) expect(config_cache.value.length).to eq 1 @@ -92,17 +96,18 @@ expect(start_time_milliseconds).to be <= cache_tokens[0].to_f expect((Utils.get_utc_now_seconds_since_epoch * 1000).floor).to be >= cache_tokens[0].to_f expect(cache_tokens[1]).to eq('test-etag') - expect(cache_tokens[2]).to eq(TEST_JSON_FORMAT % { value: '"test"' }) + expect(cache_tokens[2]).to eq(TEST_JSON_FORMAT % { value_type: SettingType::STRING, value: '{"s": "test"}' }) # Update response WebMock.stub_request(:get, Regexp.new('https://.*')) - .to_return(status: 200, body: TEST_JSON_FORMAT % { value: '"test2"' }, + .to_return(status: 200, body: TEST_JSON_FORMAT % { value_type: SettingType::STRING, value: '{"s": "test2"}' }, headers: { 'ETag' => 'test-etag' }) start_time_milliseconds = (Utils.get_utc_now_seconds_since_epoch * 1000).floor cache_policy.refresh - settings, _ = cache_policy.get_settings - expect(settings.fetch("testKey").fetch(VALUE)).to eq "test2" + config, _ = cache_policy.get_config + settings = config.fetch(FEATURE_FLAGS) + expect(settings.fetch("testKey").fetch(VALUE).fetch(STRING_VALUE)).to eq "test2" expect(stub_request).to have_been_made.times(2) expect(config_cache.value.length).to eq 1 @@ -112,7 +117,7 @@ expect(start_time_milliseconds).to be <= cache_tokens[0].to_f expect((Utils.get_utc_now_seconds_since_epoch * 1000).floor).to be >= cache_tokens[0].to_f expect(cache_tokens[1]).to eq('test-etag') - expect(cache_tokens[2]).to eq(TEST_JSON_FORMAT % { value: '"test2"' }) + expect(cache_tokens[2]).to eq(TEST_JSON_FORMAT % { value_type: SettingType::STRING, value: '{"s": "test2"}' }) cache_policy.close end @@ -129,8 +134,9 @@ expect(cache_policy.offline?).to be false expect(cache_policy.refresh.success).to be true - settings, _ = cache_policy.get_settings - expect(settings.fetch("testStringKey").fetch(VALUE)).to eq "testValue" + config, _ = cache_policy.get_config + settings = config.fetch(FEATURE_FLAGS) + expect(settings.fetch("testStringKey").fetch(VALUE).fetch(STRING_VALUE)).to eq "testValue" expect(stub_request).to have_been_made.times(1) cache_policy.set_offline @@ -166,8 +172,9 @@ expect(cache_policy.offline?).to be false expect(cache_policy.refresh.success).to be true - settings, _ = cache_policy.get_settings - expect(settings.fetch("testStringKey").fetch(VALUE)).to eq "testValue" + config, _ = cache_policy.get_config + settings = config.fetch(FEATURE_FLAGS) + expect(settings.fetch("testStringKey").fetch(VALUE).fetch(STRING_VALUE)).to eq "testValue" expect(stub_request).to have_been_made.times(1) cache_policy.close diff --git a/spec/configcat/mocks.rb b/spec/configcat/mocks.rb index d6a53af..7b20291 100644 --- a/spec/configcat/mocks.rb +++ b/spec/configcat/mocks.rb @@ -1,41 +1,61 @@ require 'configcat/interfaces' require 'json' -TEST_JSON = '{' \ - ' "p": {' \ - ' "u": "https://cdn-global.configcat.com",' \ - ' "r": 0' \ - ' },' \ - ' "f": {' \ - ' "testKey": { "v": "testValue", "t": 1, "p": [], "r": [] }' \ - ' }' \ - '}' - -TEST_JSON_FORMAT = '{ "f": { "testKey": { "v": %{value}, "p": [], "r": [] } } }' - -TEST_JSON2 = '{' \ - ' "p": {' \ - ' "u": "https://cdn-global.configcat.com",' \ - ' "r": 0' \ - ' },' \ - ' "f": {' \ - ' "testKey": { "v": "testValue", "t": 1, "p": [], "r": [] }, ' \ - ' "testKey2": { "v": "testValue2", "t": 1, "p": [], "r": [] }' \ - ' }' \ - '}' +TEST_SDK_KEY = 'configcat-sdk-test-key/0000000000000000000000' +TEST_SDK_KEY1 = 'configcat-sdk-test-key/0000000000000000000001' +TEST_SDK_KEY2 = 'configcat-sdk-test-key/0000000000000000000002' + +TEST_JSON = '{ + "p": { + "u": "https://cdn-global.configcat.com", + "r": 0 + }, + "f": { + "testKey": { "v": { "s": "testValue" }, "t": 1 } + } +}' + +TEST_JSON_FORMAT = '{ "f": { "testKey": { "t": %{value_type}, "v": %{value}, "p": [], "r": [] } } }' + +TEST_JSON2 = '{ + "p": { + "u": "https://cdn-global.configcat.com", + "r": 0 + }, + "f": { + "testKey": { "v": { "s": "testValue" }, "t": 1 }, + "testKey2": { "v": { "s": "testValue2" }, "t": 1 } + } +}' TEST_OBJECT_JSON = '{ - "p": {"u": "https://cdn-global.configcat.com", "r": 0}, + "p": { + "u": "https://cdn-global.configcat.com", + "r": 0 + }, + "s": [ + {"n": "id1", "r": [{"a": "Identifier", "c": 2, "l": ["@test1.com"]}]}, + {"n": "id2", "r": [{"a": "Identifier", "c": 2, "l": ["@test2.com"]}]} + ], "f": { - "testBoolKey": {"v": true,"t": 0, "p": [],"r": []}, - "testStringKey": {"v": "testValue", "i": "id", "t": 1, "p": [],"r": [ - {"i":"id1","v":"fake1","a":"Identifier","t":2,"c":"@test1.com"}, - {"i":"id2","v":"fake2","a":"Identifier","t":2,"c":"@test2.com"} + "testBoolKey": {"v": {"b": true}, "t": 0}, + "testStringKey": {"v": {"s": "testValue"}, "i": "id", "t": 1, "r": [ + {"c": [{"s": {"s": 0, "c": 0}}], "s": {"v": {"s": "fake1"}, "i": "id1"}}, + {"c": [{"s": {"s": 1, "c": 0}}], "s": {"v": {"s": "fake2"}, "i": "id2"}} ]}, - "testIntKey": {"v": 1,"t": 2, "p": [],"r": []}, - "testDoubleKey": {"v": 1.1,"t": 3,"p": [],"r": []}, - "key1": {"v": true, "i": "fakeId1","p": [], "r": []}, - "key2": {"v": false, "i": "fakeId2","p": [], "r": []} + "testIntKey": {"v": {"i": 1}, "t": 2}, + "testDoubleKey": {"v": {"d": 1.1}, "t": 3}, + "key1": {"v": {"b": true}, "t": 0, "i": "id3"}, + "key2": {"v": {"s": "fake4"}, "t": 1, "i": "id4", + "r": [ + {"c": [{"s": {"s": 0, "c": 0}}], "p": [ + {"p": 50, "v": {"s": "fake5"}, "i": "id5"}, {"p": 50, "v": {"s": "fake6"}, "i": "id6"} + ]} + ], + "p": [ + {"p": 50, "v": {"s": "fake7"}, "i": "id7"}, {"p": 50, "v": {"s": "fake8"}, "i": "id8"} + ] + } } }' @@ -127,7 +147,8 @@ def initialize def get_configuration(etag = '') @_value += 1 - config_json_string = TEST_JSON_FORMAT % { value: @_value } + value_string = "{ \"i\": #{@_value} }" + config_json_string = TEST_JSON_FORMAT % { value_type: SettingType::INT, value: value_string } config = JSON.parse(config_json_string) return FetchResponse.success(ConfigEntry.new(config, etag, config_json_string)) end diff --git a/spec/configcat/user_spec.rb b/spec/configcat/user_spec.rb index 965579b..100c11d 100644 --- a/spec/configcat/user_spec.rb +++ b/spec/configcat/user_spec.rb @@ -35,7 +35,12 @@ user_id = "id" email = "test@test.com" country = "country" - custom = { 'custom' => 'test' } + custom = { + 'string' => 'test', + 'datetime' => DateTime.new(2023, 9, 19, 11, 1, 35.999), + 'int' => 42, + 'float' => 3.14 + } user = User.new(user_id, email: email, country: country, custom: custom) user_json = JSON.parse(user.to_s) @@ -43,6 +48,9 @@ expect(user_json['Identifier']).to eq user_id expect(user_json['Email']).to eq email expect(user_json['Country']).to eq country - expect(user_json['Custom']).to eq custom + expect(user_json['string']).to eq 'test' + expect(user_json['int']).to eq 42 + expect(user_json['float']).to eq 3.14 + expect(user_json['datetime']).to eq "2023-09-19T11:01:35.999+0000" end end diff --git a/spec/data/comparison_attribute_conversion.json b/spec/data/comparison_attribute_conversion.json new file mode 100644 index 0000000..5a900ae --- /dev/null +++ b/spec/data/comparison_attribute_conversion.json @@ -0,0 +1,789 @@ +{ + "p": { + "u": "https://test-cdn-global.configcat.com", + "r": 0, + "s": "uM29sy1rjx71ze3ehr\u002BqCnoIpx8NZgL8V//MN7OL1aM=" + }, + "f": { + "numberToStringConversion": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "0.12345" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "numberToStringConversionInt": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "125" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "numberToStringConversionPositiveExp": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "-1.23456789e+96" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "numberToStringConversionNegativeExp": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "-1.23456789e-96" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "numberToStringConversionNaN": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "NaN" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "numberToStringConversionPositiveInf": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "Infinity" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "numberToStringConversionNegativeInf": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "-Infinity" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "dateToStringConversion": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "1680307199.999" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "dateToStringConversionNaN": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "NaN" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "dateToStringConversionPositiveInf": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "Infinity" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "dateToStringConversionNegativeInf": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "-Infinity" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "stringArrayToStringConversion": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "[\"read\",\"Write\",\" eXecute \"]" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "stringArrayToStringConversionEmpty": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "[]" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "stringArrayToStringConversionSpecialChars": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "[\"+<>%\\\"'\\\\/\\t\\r\\n\"]" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "stringArrayToStringConversionUnicode": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "[\"äöüÄÖÜçéèñışğ⢙✓😀\"]" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + } + } +} diff --git a/spec/data/comparison_attribute_trimming.json b/spec/data/comparison_attribute_trimming.json new file mode 100644 index 0000000..a42df5f --- /dev/null +++ b/spec/data/comparison_attribute_trimming.json @@ -0,0 +1,985 @@ +{ + "p": { + "u": "https://test-cdn-eu.configcat.com", + "r": 0, + "s": "VjBfGYcmyHzLBv5EINgSBbX6/rYevYGWQhF3Zk5t8i4=" + }, + "f": { + "arraycontainsanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Country", + "c": 34, + "l": [ + "USA" + ] + } + } + ], + "s": { + "v": { + "s": "34 trim" + }, + "i": "99c90883" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "9c66d87c" + }, + "arraycontainsanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Country", + "c": 26, + "l": [ + "09d5761537a8136eb7fc45a53917b51cb9dcd2bb9b62ffa24ace0e8a7600a3c7" + ] + } + } + ], + "s": { + "v": { + "s": "26 trim" + }, + "i": "706c94b6" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "3b342be3" + }, + "arraynotcontainsanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Country", + "c": 35, + "l": [ + "USA" + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "4eeb2176" + } + } + ], + "v": { + "s": "35 trim" + }, + "i": "98bc8ebb" + }, + "arraynotcontainsanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Country", + "c": 27, + "l": [ + "99d06b6b3669b906803c285267f76fe4e2ccc194b00801ab07f2fd49939b6960" + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "8f248790" + } + } + ], + "v": { + "s": "27 trim" + }, + "i": "278ddbe9" + }, + "endswithanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 32, + "l": [ + "12345" + ] + } + } + ], + "s": { + "v": { + "s": "32 trim" + }, + "i": "0ac9e321" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "777456df" + }, + "endswithanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 24, + "l": [ + "5_7eb158c29b48b62cec860dffc459171edbfeef458bcc8e8bb62956d823eef3df" + ] + } + } + ], + "s": { + "v": { + "s": "24 trim" + }, + "i": "0364bf98" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "2f6fc77b" + }, + "equals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 28, + "s": "12345" + } + } + ], + "s": { + "v": { + "s": "28 trim" + }, + "i": "f2a682ca" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "0f806923" + }, + "equalshashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 20, + "s": "ea0d05859bb737105eea40bc605f6afd542c8f50f8497cd21ace38e731d7eef0" + } + } + ], + "s": { + "v": { + "s": "20 trim" + }, + "i": "6f1798e9" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "771ecd4d" + }, + "isnotoneof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 1, + "l": [ + "12345" + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "79d49e05" + } + } + ], + "v": { + "s": "1 trim" + }, + "i": "61d13448" + }, + "isnotoneofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 1, + "l": [ + "12345" + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "1c2df623" + } + } + ], + "v": { + "s": "17 trim" + }, + "i": "0bc3daa1" + }, + "isoneof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 0, + "l": [ + "12345" + ] + } + } + ], + "s": { + "v": { + "s": "0 trim" + }, + "i": "308f0749" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "90984858" + }, + "isoneofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 16, + "l": [ + "1765b470044971bbc19e7bed10112199c5da9c626455f86be109fef96e747911" + ] + } + } + ], + "s": { + "v": { + "s": "16 trim" + }, + "i": "cd78a85d" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "30b9483f" + }, + "notendswithanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 33, + "l": [ + "12345" + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "b0d7203e" + } + } + ], + "v": { + "s": "33 trim" + }, + "i": "89740c7e" + }, + "notendswithanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 25, + "l": [ + "5_2a338d3beb8ebe2e711d198420d04e2627e39501c2fcc7d5b3b8d93540691097" + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "059f59e3" + } + } + ], + "v": { + "s": "25 trim" + }, + "i": "c1e95c48" + }, + "notequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 29, + "s": "12345" + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "af1f1e95" + } + } + ], + "v": { + "s": "29 trim" + }, + "i": "219e6bac" + }, + "notequalshashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 21, + "s": "650fe0e8e86030b5f73ccd77e6532f307adf82506048a22f02d95386206ecea1" + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "9fe2b26b" + } + } + ], + "v": { + "s": "21 trim" + }, + "i": "9211e9f1" + }, + "notstartwithanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 31, + "l": [ + "12345" + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "ebe3ed2d" + } + } + ], + "v": { + "s": "31 trim" + }, + "i": "7deb7219" + }, + "notstartwithanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 23, + "l": [ + "5_586ab2ec61946cb1457d4af170d88e7f14e655d9debf352b4ab6bf5bf77df3f7" + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "7b606e54" + } + } + ], + "v": { + "s": "23 trim" + }, + "i": "edec740e" + }, + "semvergreater": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 8, + "s": "0.1.1" + } + } + ], + "s": { + "v": { + "s": "8 trim" + }, + "i": "25edfdc1" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "cb0224fd" + }, + "semvergreaterequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 9, + "s": "0.1.1" + } + } + ], + "s": { + "v": { + "s": "9 trim" + }, + "i": "d8960b43" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "530ea45c" + }, + "semverisnotoneof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 5, + "l": [ + "1.0.1" + ] + } + } + ], + "s": { + "v": { + "s": "5 trim" + }, + "i": "cb1bad57" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "4a7025a4" + }, + "semverisoneof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 4, + "l": [ + "1.0.0" + ] + } + } + ], + "s": { + "v": { + "s": "4 trim" + }, + "i": "6cc37494" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "842a56b5" + }, + "semverless": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 6, + "s": "1.0.1" + } + } + ], + "s": { + "v": { + "s": "6 trim" + }, + "i": "64c04b67" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "ae58de40" + }, + "semverlessequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 7, + "s": "1.0.1" + } + } + ], + "s": { + "v": { + "s": "7 trim" + }, + "i": "7c62748d" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "631a1888" + }, + "startwithanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 30, + "l": [ + "12345" + ] + } + } + ], + "s": { + "v": { + "s": "30 trim" + }, + "i": "475a9c4f" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "5a73105a" + }, + "startwithanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 22, + "l": [ + "5_67a323069ee45fef4ccd8365007d4713f7a3bc87764943b1139e8e50d1aee8fd" + ] + } + } + ], + "s": { + "v": { + "s": "22 trim" + }, + "i": "7650175d" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "a38edbee" + }, + "dateafter": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Date", + "c": 19, + "d": 1705251600 + } + } + ], + "s": { + "v": { + "s": "19 trim" + }, + "i": "83e580ce" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "1c12e0cc" + }, + "datebefore": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Date", + "c": 18, + "d": 1705255200 + } + } + ], + "s": { + "v": { + "s": "18 trim" + }, + "i": "34614b07" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "26d4f328" + }, + "numberequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Number", + "c": 10, + "d": 3 + } + } + ], + "s": { + "v": { + "s": "10 trim" + }, + "i": "6a8c0a08" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "7b8e49b9" + }, + "numbergreater": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Number", + "c": 14, + "d": 2 + } + } + ], + "s": { + "v": { + "s": "14 trim" + }, + "i": "2037a7a4" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "902f9bd9" + }, + "numbergreaterequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Number", + "c": 15, + "d": 2 + } + } + ], + "s": { + "v": { + "s": "15 trim" + }, + "i": "527c49d2" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "2280c961" + }, + "numberless": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Number", + "c": 12, + "d": 4 + } + } + ], + "s": { + "v": { + "s": "12 trim" + }, + "i": "c454f775" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "ec935943" + }, + "numberlessequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Number", + "c": 13, + "d": 4 + } + } + ], + "s": { + "v": { + "s": "13 trim" + }, + "i": "1e31aed8" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "1d53c679" + }, + "numbernotequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Number", + "c": 11, + "d": 6 + } + } + ], + "s": { + "v": { + "s": "11 trim" + }, + "i": "e8d7cf05" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "21c749a7" + }, + "containsanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 2, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "f750380a" + } + } + ], + "v": { + "s": "2 trim" + }, + "i": "c3ab37cf" + }, + "notcontainsanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 3, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "3 trim" + }, + "i": "4b8760c4" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "f91ecf16" + } + } +} \ No newline at end of file diff --git a/spec/data/comparison_value_trimming.json b/spec/data/comparison_value_trimming.json new file mode 100644 index 0000000..db91703 --- /dev/null +++ b/spec/data/comparison_value_trimming.json @@ -0,0 +1,777 @@ +{ + "p": { + "u": "https://test-cdn-eu.configcat.com", + "r": 0, + "s": "zsVN1DQ9Oa2FjFc96MvPfMM5Vs+KKV00NyybJZipyf4=" + }, + "f": { + "arraycontainsanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Country", + "c": 34, + "l": [ + " USA " + ] + } + } + ], + "s": { + "v": { + "s": "34 trim" + }, + "i": "99c90883" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "9c66d87c" + }, + "arraycontainsanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Country", + "c": 26, + "l": [ + " 028fdb841bf3b2cc27fce407da08f87acd3a58a08c67d819cdb9351857b14237 " + ] + } + } + ], + "s": { + "v": { + "s": "26 trim" + }, + "i": "706c94b6" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "3b342be3" + }, + "arraynotcontainsanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Country", + "c": 35, + "l": [ + " USA " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "4eeb2176" + } + } + ], + "v": { + "s": "35 trim" + }, + "i": "98bc8ebb" + }, + "arraynotcontainsanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Country", + "c": 27, + "l": [ + " 60b747c290642863f9a6c68773ed309a9fb02c6c1ae65c77037046918f4c1d3c " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "8f248790" + } + } + ], + "v": { + "s": "27 trim" + }, + "i": "278ddbe9" + }, + "containsanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 2, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "2 trim" + }, + "i": "f750380a" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "c3ab37cf" + }, + "endswithanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 32, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "32 trim" + }, + "i": "0ac9e321" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "777456df" + }, + "endswithanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 24, + "l": [ + " 5_a6ce5e2838d4e0c27cd705c90f39e60d79056062983c39951668cf947ec406c2 " + ] + } + } + ], + "s": { + "v": { + "s": "24 trim" + }, + "i": "0364bf98" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "2f6fc77b" + }, + "equals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 28, + "s": " 12345 " + } + } + ], + "s": { + "v": { + "s": "28 trim" + }, + "i": "f2a682ca" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "0f806923" + }, + "equalshashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 20, + "s": " a2868640b1fe24c98e50b168756d83fd03779dd4349d6ddab5d7d6ef8dad13bd " + } + } + ], + "s": { + "v": { + "s": "20 trim" + }, + "i": "6f1798e9" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "771ecd4d" + }, + "isnotoneof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 1, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "79d49e05" + } + } + ], + "v": { + "s": "1 trim" + }, + "i": "61d13448" + }, + "isnotoneofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 1, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "1c2df623" + } + } + ], + "v": { + "s": "17 trim" + }, + "i": "0bc3daa1" + }, + "isoneof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 0, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "0 trim" + }, + "i": "308f0749" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "90984858" + }, + "isoneofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 16, + "l": [ + " 55ce90920d20fc0bf8078471062a85f82cc5ea2226012a901a5045775bace0f4 " + ] + } + } + ], + "s": { + "v": { + "s": "16 trim" + }, + "i": "cd78a85d" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "30b9483f" + }, + "notcontainsanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 3, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "4b8760c4" + } + } + ], + "v": { + "s": "3 trim" + }, + "i": "f91ecf16" + }, + "notendswithanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 33, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "b0d7203e" + } + } + ], + "v": { + "s": "33 trim" + }, + "i": "89740c7e" + }, + "notendswithanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 25, + "l": [ + " 5_c517fc957907e30b6a790540a20172a3a5d3a7458a85e340a7b1a1ac982be278 " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "059f59e3" + } + } + ], + "v": { + "s": "25 trim" + }, + "i": "c1e95c48" + }, + "notequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 29, + "s": " 12345 " + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "af1f1e95" + } + } + ], + "v": { + "s": "29 trim" + }, + "i": "219e6bac" + }, + "notequalshashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 21, + "s": " 31ceae14b865b0842e93fdc3a42a7e45780ccc41772ca9355db50e09d81e13ef " + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "9fe2b26b" + } + } + ], + "v": { + "s": "21 trim" + }, + "i": "9211e9f1" + }, + "notstartwithanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 31, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "ebe3ed2d" + } + } + ], + "v": { + "s": "31 trim" + }, + "i": "7deb7219" + }, + "notstartwithanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 23, + "l": [ + " 5_3643bbdd1bce4021fe4dbd55e6cc2f4902e4f50e592597d1a2d0e944fb7dfb42 " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "7b606e54" + } + } + ], + "v": { + "s": "23 trim" + }, + "i": "edec740e" + }, + "semvergreater": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 8, + "s": " 0.1.1 " + } + } + ], + "s": { + "v": { + "s": "8 trim" + }, + "i": "25edfdc1" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "cb0224fd" + }, + "semvergreaterequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 9, + "s": " 0.1.1 " + } + } + ], + "s": { + "v": { + "s": "9 trim" + }, + "i": "d8960b43" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "530ea45c" + }, + "semverisnotoneof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 5, + "l": [ + " 1.0.1 " + ] + } + } + ], + "s": { + "v": { + "s": "5 trim" + }, + "i": "cb1bad57" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "4a7025a4" + }, + "semverisoneof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 4, + "l": [ + " 1.0.0 " + ] + } + } + ], + "s": { + "v": { + "s": "4 trim" + }, + "i": "6cc37494" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "842a56b5" + }, + "semverless": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 6, + "s": " 1.0.1 " + } + } + ], + "s": { + "v": { + "s": "6 trim" + }, + "i": "64c04b67" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "ae58de40" + }, + "semverlessequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 7, + "s": " 1.0.1 " + } + } + ], + "s": { + "v": { + "s": "7 trim" + }, + "i": "7c62748d" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "631a1888" + }, + "startwithanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 30, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "30 trim" + }, + "i": "475a9c4f" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "5a73105a" + }, + "startwithanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 22, + "l": [ + " 5_3e052709552ca9d5bd6c459cb7ab0389f3210f6aafc3d006a2481635e9614a7c " + ] + } + } + ], + "s": { + "v": { + "s": "22 trim" + }, + "i": "7650175d" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "a38edbee" + } + } +} \ No newline at end of file diff --git a/spec/data/evaluation/1_targeting_rule.json b/spec/data/evaluation/1_targeting_rule.json new file mode 100644 index 0000000..596bd2b --- /dev/null +++ b/spec/data/evaluation/1_targeting_rule.json @@ -0,0 +1,41 @@ +{ + "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d62463-86ec-8fde-f5b5-1c5c426fc830/244cf8b0-f604-11e8-b543-f23c917f9d8d", + "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", + "tests": [ + { + "key": "stringContainsDogDefaultCat", + "defaultValue": "default", + "returnValue": "Cat", + "expectedLog": "1_rule_no_user.txt" + }, + { + "key": "stringContainsDogDefaultCat", + "defaultValue": "default", + "user": { + "Identifier": "12345" + }, + "returnValue": "Cat", + "expectedLog": "1_rule_no_targeted_attribute.txt" + }, + { + "key": "stringContainsDogDefaultCat", + "defaultValue": "default", + "user": { + "Identifier": "12345", + "Email": "joe@example.com" + }, + "returnValue": "Cat", + "expectedLog": "1_rule_not_matching_targeted_attribute.txt" + }, + { + "key": "stringContainsDogDefaultCat", + "defaultValue": "default", + "user": { + "Identifier": "12345", + "Email": "joe@configcat.com" + }, + "returnValue": "Dog", + "expectedLog": "1_rule_matching_targeted_attribute.txt" + } + ] +} diff --git a/spec/data/evaluation/1_targeting_rule/1_rule_matching_targeted_attribute.txt b/spec/data/evaluation/1_targeting_rule/1_rule_matching_targeted_attribute.txt new file mode 100644 index 0000000..f05c6f6 --- /dev/null +++ b/spec/data/evaluation/1_targeting_rule/1_rule_matching_targeted_attribute.txt @@ -0,0 +1,4 @@ +INFO [5000] Evaluating 'stringContainsDogDefaultCat' for User '{"Identifier":"12345","Email":"joe@configcat.com"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN 'Dog' => MATCH, applying rule + Returning 'Dog'. diff --git a/spec/data/evaluation/1_targeting_rule/1_rule_no_targeted_attribute.txt b/spec/data/evaluation/1_targeting_rule/1_rule_no_targeted_attribute.txt new file mode 100644 index 0000000..34fbb6d --- /dev/null +++ b/spec/data/evaluation/1_targeting_rule/1_rule_no_targeted_attribute.txt @@ -0,0 +1,6 @@ +WARN [3003] Cannot evaluate condition (User.Email CONTAINS ANY OF ['@configcat.com']) for setting 'stringContainsDogDefaultCat' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'stringContainsDogDefaultCat' for User '{"Identifier":"12345"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN 'Dog' => cannot evaluate, the User.Email attribute is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'Cat'. diff --git a/spec/data/evaluation/1_targeting_rule/1_rule_no_user.txt b/spec/data/evaluation/1_targeting_rule/1_rule_no_user.txt new file mode 100644 index 0000000..7a81a45 --- /dev/null +++ b/spec/data/evaluation/1_targeting_rule/1_rule_no_user.txt @@ -0,0 +1,6 @@ +WARN [3001] Cannot evaluate targeting rules and % options for setting 'stringContainsDogDefaultCat' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'stringContainsDogDefaultCat' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN 'Dog' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'Cat'. diff --git a/spec/data/evaluation/1_targeting_rule/1_rule_not_matching_targeted_attribute.txt b/spec/data/evaluation/1_targeting_rule/1_rule_not_matching_targeted_attribute.txt new file mode 100644 index 0000000..49d1252 --- /dev/null +++ b/spec/data/evaluation/1_targeting_rule/1_rule_not_matching_targeted_attribute.txt @@ -0,0 +1,4 @@ +INFO [5000] Evaluating 'stringContainsDogDefaultCat' for User '{"Identifier":"12345","Email":"joe@example.com"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN 'Dog' => no match + Returning 'Cat'. diff --git a/spec/data/evaluation/2_targeting_rules.json b/spec/data/evaluation/2_targeting_rules.json new file mode 100644 index 0000000..5cf8a3c --- /dev/null +++ b/spec/data/evaluation/2_targeting_rules.json @@ -0,0 +1,41 @@ +{ + "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d62463-86ec-8fde-f5b5-1c5c426fc830/244cf8b0-f604-11e8-b543-f23c917f9d8d", + "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", + "tests": [ + { + "key": "stringIsInDogDefaultCat", + "defaultValue": "default", + "returnValue": "Cat", + "expectedLog": "2_rules_no_user.txt" + }, + { + "key": "stringIsInDogDefaultCat", + "defaultValue": "default", + "user": { + "Identifier": "12345" + }, + "returnValue": "Cat", + "expectedLog": "2_rules_no_targeted_attribute.txt" + }, + { + "key": "stringIsInDogDefaultCat", + "defaultValue": "default", + "user": { + "Identifier": "12345", + "Custom1": "user" + }, + "returnValue": "Cat", + "expectedLog": "2_rules_not_matching_targeted_attribute.txt" + }, + { + "key": "stringIsInDogDefaultCat", + "defaultValue": "default", + "user": { + "Identifier": "12345", + "Custom1": "admin" + }, + "returnValue": "Dog", + "expectedLog": "2_rules_matching_targeted_attribute.txt" + } + ] +} diff --git a/spec/data/evaluation/2_targeting_rules/2_rules_matching_targeted_attribute.txt b/spec/data/evaluation/2_targeting_rules/2_rules_matching_targeted_attribute.txt new file mode 100644 index 0000000..e125518 --- /dev/null +++ b/spec/data/evaluation/2_targeting_rules/2_rules_matching_targeted_attribute.txt @@ -0,0 +1,7 @@ +WARN [3003] Cannot evaluate condition (User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com']) for setting 'stringIsInDogDefaultCat' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'stringIsInDogDefaultCat' for User '{"Identifier":"12345","Custom1":"admin"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com'] THEN 'Dog' => cannot evaluate, the User.Email attribute is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF User.Custom1 IS ONE OF ['admin'] THEN 'Dog' => MATCH, applying rule + Returning 'Dog'. diff --git a/spec/data/evaluation/2_targeting_rules/2_rules_no_targeted_attribute.txt b/spec/data/evaluation/2_targeting_rules/2_rules_no_targeted_attribute.txt new file mode 100644 index 0000000..7b7d6be --- /dev/null +++ b/spec/data/evaluation/2_targeting_rules/2_rules_no_targeted_attribute.txt @@ -0,0 +1,9 @@ +WARN [3003] Cannot evaluate condition (User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com']) for setting 'stringIsInDogDefaultCat' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +WARN [3003] Cannot evaluate condition (User.Custom1 IS ONE OF ['admin']) for setting 'stringIsInDogDefaultCat' (the User.Custom1 attribute is missing). You should set the User.Custom1 attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'stringIsInDogDefaultCat' for User '{"Identifier":"12345"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com'] THEN 'Dog' => cannot evaluate, the User.Email attribute is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF User.Custom1 IS ONE OF ['admin'] THEN 'Dog' => cannot evaluate, the User.Custom1 attribute is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'Cat'. diff --git a/spec/data/evaluation/2_targeting_rules/2_rules_no_user.txt b/spec/data/evaluation/2_targeting_rules/2_rules_no_user.txt new file mode 100644 index 0000000..3199b32 --- /dev/null +++ b/spec/data/evaluation/2_targeting_rules/2_rules_no_user.txt @@ -0,0 +1,8 @@ +WARN [3001] Cannot evaluate targeting rules and % options for setting 'stringIsInDogDefaultCat' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'stringIsInDogDefaultCat' + Evaluating targeting rules and applying the first match if any: + - IF User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com'] THEN 'Dog' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF User.Custom1 IS ONE OF ['admin'] THEN 'Dog' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'Cat'. diff --git a/spec/data/evaluation/2_targeting_rules/2_rules_not_matching_targeted_attribute.txt b/spec/data/evaluation/2_targeting_rules/2_rules_not_matching_targeted_attribute.txt new file mode 100644 index 0000000..a562831 --- /dev/null +++ b/spec/data/evaluation/2_targeting_rules/2_rules_not_matching_targeted_attribute.txt @@ -0,0 +1,7 @@ +WARN [3003] Cannot evaluate condition (User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com']) for setting 'stringIsInDogDefaultCat' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'stringIsInDogDefaultCat' for User '{"Identifier":"12345","Custom1":"user"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com'] THEN 'Dog' => cannot evaluate, the User.Email attribute is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF User.Custom1 IS ONE OF ['admin'] THEN 'Dog' => no match + Returning 'Cat'. diff --git a/spec/data/evaluation/and_rules.json b/spec/data/evaluation/and_rules.json new file mode 100644 index 0000000..c6ed879 --- /dev/null +++ b/spec/data/evaluation/and_rules.json @@ -0,0 +1,22 @@ +{ + "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9d5e-4988-891c-fd4a45790bd1/08dbc325-9ebd-4587-8171-88f76a3004cb", + "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/ByMO9yZNn02kXcm72lnY1A", + "tests": [ + { + "key": "emailAnd", + "defaultValue": "default", + "returnValue": "Cat", + "expectedLog": "and_rules_no_user.txt" + }, + { + "key": "emailAnd", + "defaultValue": "default", + "user": { + "Identifier": "12345", + "Email": "jane@configcat.com" + }, + "returnValue": "Cat", + "expectedLog": "and_rules_user.txt" + } + ] +} diff --git a/spec/data/evaluation/and_rules/and_rules_no_user.txt b/spec/data/evaluation/and_rules/and_rules_no_user.txt new file mode 100644 index 0000000..8f4408c --- /dev/null +++ b/spec/data/evaluation/and_rules/and_rules_no_user.txt @@ -0,0 +1,7 @@ +WARN [3001] Cannot evaluate targeting rules and % options for setting 'emailAnd' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'emailAnd' + Evaluating targeting rules and applying the first match if any: + - IF User.Email STARTS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN 'Dog' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'Cat'. diff --git a/spec/data/evaluation/and_rules/and_rules_user.txt b/spec/data/evaluation/and_rules/and_rules_user.txt new file mode 100644 index 0000000..92c59ce --- /dev/null +++ b/spec/data/evaluation/and_rules/and_rules_user.txt @@ -0,0 +1,7 @@ +INFO [5000] Evaluating 'emailAnd' for User '{"Identifier":"12345","Email":"jane@configcat.com"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email STARTS WITH ANY OF [<1 hashed value>] => true + AND User.Email CONTAINS ANY OF ['@'] => true + AND User.Email ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN 'Dog' => no match + Returning 'Cat'. diff --git a/spec/data/evaluation/comparators.json b/spec/data/evaluation/comparators.json new file mode 100644 index 0000000..5d5631e --- /dev/null +++ b/spec/data/evaluation/comparators.json @@ -0,0 +1,20 @@ +{ + "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9a6b-4947-84e2-91529248278a/08dbc325-9ebd-4587-8171-88f76a3004cb", + "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", + "tests": [ + { + "key": "allinone", + "defaultValue": "", + "user": { + "Identifier": "12345", + "Email": "joe@example.com", + "Country": "[\"USA\"]", + "Version": "1.0.0", + "Number": "1.0", + "Date": "1693497500" + }, + "returnValue": "default", + "expectedLog": "allinone.txt" + } + ] +} diff --git a/spec/data/evaluation/comparators/allinone.txt b/spec/data/evaluation/comparators/allinone.txt new file mode 100644 index 0000000..84e9b32 --- /dev/null +++ b/spec/data/evaluation/comparators/allinone.txt @@ -0,0 +1,57 @@ +INFO [5000] Evaluating 'allinone' for User '{"Identifier":"12345","Email":"joe@example.com","Country":"[\"USA\"]","Version":"1.0.0","Number":"1.0","Date":"1693497500"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email EQUALS '' => true + AND User.Email NOT EQUALS '' => false, skipping the remaining AND conditions + THEN '1h' => no match + - IF User.Email EQUALS 'joe@example.com' => true + AND User.Email NOT EQUALS 'joe@example.com' => false, skipping the remaining AND conditions + THEN '1c' => no match + - IF User.Email IS ONE OF [<1 hashed value>] => true + AND User.Email IS NOT ONE OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN '2h' => no match + - IF User.Email IS ONE OF ['joe@example.com'] => true + AND User.Email IS NOT ONE OF ['joe@example.com'] => false, skipping the remaining AND conditions + THEN '2c' => no match + - IF User.Email STARTS WITH ANY OF [<1 hashed value>] => true + AND User.Email NOT STARTS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN '3h' => no match + - IF User.Email STARTS WITH ANY OF ['joe@'] => true + AND User.Email NOT STARTS WITH ANY OF ['joe@'] => false, skipping the remaining AND conditions + THEN '3c' => no match + - IF User.Email ENDS WITH ANY OF [<1 hashed value>] => true + AND User.Email NOT ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN '4h' => no match + - IF User.Email ENDS WITH ANY OF ['@example.com'] => true + AND User.Email NOT ENDS WITH ANY OF ['@example.com'] => false, skipping the remaining AND conditions + THEN '4c' => no match + - IF User.Email CONTAINS ANY OF ['e@e'] => true + AND User.Email NOT CONTAINS ANY OF ['e@e'] => false, skipping the remaining AND conditions + THEN '5' => no match + - IF User.Version IS ONE OF ['1.0.0'] => true + AND User.Version IS NOT ONE OF ['1.0.0'] => false, skipping the remaining AND conditions + THEN '6' => no match + - IF User.Version < '1.0.1' => true + AND User.Version >= '1.0.1' => false, skipping the remaining AND conditions + THEN '7' => no match + - IF User.Version > '0.9.9' => true + AND User.Version <= '0.9.9' => false, skipping the remaining AND conditions + THEN '8' => no match + - IF User.Number = '1' => true + AND User.Number != '1' => false, skipping the remaining AND conditions + THEN '9' => no match + - IF User.Number < '1.1' => true + AND User.Number >= '1.1' => false, skipping the remaining AND conditions + THEN '10' => no match + - IF User.Number > '0.9' => true + AND User.Number <= '0.9' => false, skipping the remaining AND conditions + THEN '11' => no match + - IF User.Date BEFORE '1693497600' (2023-08-31T16:00:00.000Z UTC) => true + AND User.Date AFTER '1693497600' (2023-08-31T16:00:00.000Z UTC) => false, skipping the remaining AND conditions + THEN '12' => no match + - IF User.Country ARRAY CONTAINS ANY OF [<1 hashed value>] => true + AND User.Country ARRAY NOT CONTAINS ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN '13h' => no match + - IF User.Country ARRAY CONTAINS ANY OF ['USA'] => true + AND User.Country ARRAY NOT CONTAINS ANY OF ['USA'] => false, skipping the remaining AND conditions + THEN '13c' => no match + Returning 'default'. diff --git a/spec/data/evaluation/epoch_date_validation.json b/spec/data/evaluation/epoch_date_validation.json new file mode 100644 index 0000000..e916d21 --- /dev/null +++ b/spec/data/evaluation/epoch_date_validation.json @@ -0,0 +1,16 @@ +{ + "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9a6b-4947-84e2-91529248278a/08dbc325-9ebd-4587-8171-88f76a3004cb", + "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", + "tests": [ + { + "key": "boolTrueIn202304", + "defaultValue": true, + "returnValue": false, + "expectedLog": "date_error.txt", + "user": { + "Identifier": "12345", + "Custom1": "2023.04.10" + } + } + ] +} diff --git a/spec/data/evaluation/epoch_date_validation/date_error.txt b/spec/data/evaluation/epoch_date_validation/date_error.txt new file mode 100644 index 0000000..4c8bd68 --- /dev/null +++ b/spec/data/evaluation/epoch_date_validation/date_error.txt @@ -0,0 +1,7 @@ +WARN [3004] Cannot evaluate condition (User.Custom1 AFTER '1680307200' (2023-04-01T00:00:00.000Z UTC)) for setting 'boolTrueIn202304' ('2023.04.10' is not a valid Unix timestamp (number of seconds elapsed since Unix epoch)). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. +INFO [5000] Evaluating 'boolTrueIn202304' for User '{"Identifier":"12345","Custom1":"2023.04.10"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Custom1 AFTER '1680307200' (2023-04-01T00:00:00.000Z UTC) => false, skipping the remaining AND conditions + THEN 'true' => cannot evaluate, the User.Custom1 attribute is invalid ('2023.04.10' is not a valid Unix timestamp (number of seconds elapsed since Unix epoch)) + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'false'. diff --git a/spec/data/evaluation/list_truncation.json b/spec/data/evaluation/list_truncation.json new file mode 100644 index 0000000..64e9426 --- /dev/null +++ b/spec/data/evaluation/list_truncation.json @@ -0,0 +1,14 @@ +{ + "jsonOverride": "test_list_truncation.json", + "tests": [ + { + "key": "booleanKey1", + "defaultValue": false, + "user": { + "Identifier": "12" + }, + "returnValue": true, + "expectedLog": "list_truncation.txt" + } + ] +} diff --git a/spec/data/evaluation/list_truncation/list_truncation.txt b/spec/data/evaluation/list_truncation/list_truncation.txt new file mode 100644 index 0000000..10a0195 --- /dev/null +++ b/spec/data/evaluation/list_truncation/list_truncation.txt @@ -0,0 +1,7 @@ +INFO [5000] Evaluating 'booleanKey1' for User '{"Identifier":"12"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Identifier CONTAINS ANY OF ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'] => true + AND User.Identifier CONTAINS ANY OF ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', ... <1 more value>] => true + AND User.Identifier CONTAINS ANY OF ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', ... <2 more values>] => true + THEN 'true' => MATCH, applying rule + Returning 'true'. diff --git a/spec/data/evaluation/list_truncation/test_list_truncation.json b/spec/data/evaluation/list_truncation/test_list_truncation.json new file mode 100644 index 0000000..6fdde45 --- /dev/null +++ b/spec/data/evaluation/list_truncation/test_list_truncation.json @@ -0,0 +1,83 @@ +{ + "p": { + "u": "https://cdn-global.configcat.com", + "r": 0, + "s": "test-salt" + }, + "f": { + "booleanKey1": { + "t": 0, + "v": { + "b": false + }, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 2, + "l": [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "u": { + "a": "Identifier", + "c": 2, + "l": [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11" + ] + } + }, + { + "u": { + "a": "Identifier", + "c": 2, + "l": [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12" + ] + } + } + ], + "s": { + "v": { + "b": true + } + } + } + ] + } + } +} diff --git a/spec/data/evaluation/number_validation.json b/spec/data/evaluation/number_validation.json new file mode 100644 index 0000000..640cf3d --- /dev/null +++ b/spec/data/evaluation/number_validation.json @@ -0,0 +1,16 @@ +{ + "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d747f0-5986-c2ef-eef3-ec778e32e10a/244cf8b0-f604-11e8-b543-f23c917f9d8d", + "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/uGyK3q9_ckmdxRyI7vjwCw", + "tests": [ + { + "key": "number", + "defaultValue": "default", + "returnValue": "Default", + "expectedLog": "number_error.txt", + "user": { + "Identifier": "12345", + "Custom1": "not_a_number" + } + } + ] +} diff --git a/spec/data/evaluation/number_validation/number_error.txt b/spec/data/evaluation/number_validation/number_error.txt new file mode 100644 index 0000000..fd5e575 --- /dev/null +++ b/spec/data/evaluation/number_validation/number_error.txt @@ -0,0 +1,6 @@ +WARN [3004] Cannot evaluate condition (User.Custom1 != '5') for setting 'number' ('not_a_number' is not a valid decimal number). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. +INFO [5000] Evaluating 'number' for User '{"Identifier":"12345","Custom1":"not_a_number"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Custom1 != '5' THEN '<>5' => cannot evaluate, the User.Custom1 attribute is invalid ('not_a_number' is not a valid decimal number) + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'Default'. diff --git a/spec/data/evaluation/options_after_targeting_rule.json b/spec/data/evaluation/options_after_targeting_rule.json new file mode 100644 index 0000000..803840e --- /dev/null +++ b/spec/data/evaluation/options_after_targeting_rule.json @@ -0,0 +1,41 @@ +{ + "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d62463-86ec-8fde-f5b5-1c5c426fc830/244cf8b0-f604-11e8-b543-f23c917f9d8d", + "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", + "tests": [ + { + "key": "integer25One25Two25Three25FourAdvancedRules", + "defaultValue": 42, + "returnValue": -1, + "expectedLog": "options_after_targeting_rule_no_user.txt" + }, + { + "key": "integer25One25Two25Three25FourAdvancedRules", + "defaultValue": 42, + "user": { + "Identifier": "12345" + }, + "returnValue": 2, + "expectedLog": "options_after_targeting_rule_no_targeted_attribute.txt" + }, + { + "key": "integer25One25Two25Three25FourAdvancedRules", + "defaultValue": 42, + "user": { + "Identifier": "12345", + "Email": "joe@example.com" + }, + "returnValue": 2, + "expectedLog": "options_after_targeting_rule_not_matching_targeted_attribute.txt" + }, + { + "key": "integer25One25Two25Three25FourAdvancedRules", + "defaultValue": 42, + "user": { + "Identifier": "12345", + "Email": "joe@configcat.com" + }, + "returnValue": 5, + "expectedLog": "options_after_targeting_rule_matching_targeted_attribute.txt" + } + ] +} diff --git a/spec/data/evaluation/options_after_targeting_rule/options_after_targeting_rule_matching_targeted_attribute.txt b/spec/data/evaluation/options_after_targeting_rule/options_after_targeting_rule_matching_targeted_attribute.txt new file mode 100644 index 0000000..6815fa3 --- /dev/null +++ b/spec/data/evaluation/options_after_targeting_rule/options_after_targeting_rule_matching_targeted_attribute.txt @@ -0,0 +1,4 @@ +INFO [5000] Evaluating 'integer25One25Two25Three25FourAdvancedRules' for User '{"Identifier":"12345","Email":"joe@configcat.com"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN '5' => MATCH, applying rule + Returning '5'. diff --git a/spec/data/evaluation/options_after_targeting_rule/options_after_targeting_rule_no_targeted_attribute.txt b/spec/data/evaluation/options_after_targeting_rule/options_after_targeting_rule_no_targeted_attribute.txt new file mode 100644 index 0000000..e4e3f0e --- /dev/null +++ b/spec/data/evaluation/options_after_targeting_rule/options_after_targeting_rule_no_targeted_attribute.txt @@ -0,0 +1,9 @@ +WARN [3003] Cannot evaluate condition (User.Email CONTAINS ANY OF ['@configcat.com']) for setting 'integer25One25Two25Three25FourAdvancedRules' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'integer25One25Two25Three25FourAdvancedRules' for User '{"Identifier":"12345"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN '5' => cannot evaluate, the User.Email attribute is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Evaluating % options based on the User.Identifier attribute: + - Computing hash in the [0..99] range from User.Identifier => 25 (this value is sticky and consistent across all SDKs) + - Hash value 25 selects % option 2 (25%), '2'. + Returning '2'. diff --git a/spec/data/evaluation/options_after_targeting_rule/options_after_targeting_rule_no_user.txt b/spec/data/evaluation/options_after_targeting_rule/options_after_targeting_rule_no_user.txt new file mode 100644 index 0000000..f7ac102 --- /dev/null +++ b/spec/data/evaluation/options_after_targeting_rule/options_after_targeting_rule_no_user.txt @@ -0,0 +1,7 @@ +WARN [3001] Cannot evaluate targeting rules and % options for setting 'integer25One25Two25Three25FourAdvancedRules' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'integer25One25Two25Three25FourAdvancedRules' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN '5' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Skipping % options because the User Object is missing. + Returning '-1'. diff --git a/spec/data/evaluation/options_after_targeting_rule/options_after_targeting_rule_not_matching_targeted_attribute.txt b/spec/data/evaluation/options_after_targeting_rule/options_after_targeting_rule_not_matching_targeted_attribute.txt new file mode 100644 index 0000000..c412e5a --- /dev/null +++ b/spec/data/evaluation/options_after_targeting_rule/options_after_targeting_rule_not_matching_targeted_attribute.txt @@ -0,0 +1,7 @@ +INFO [5000] Evaluating 'integer25One25Two25Three25FourAdvancedRules' for User '{"Identifier":"12345","Email":"joe@example.com"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN '5' => no match + Evaluating % options based on the User.Identifier attribute: + - Computing hash in the [0..99] range from User.Identifier => 25 (this value is sticky and consistent across all SDKs) + - Hash value 25 selects % option 2 (25%), '2'. + Returning '2'. diff --git a/spec/data/evaluation/options_based_on_custom_attr.json b/spec/data/evaluation/options_based_on_custom_attr.json new file mode 100644 index 0000000..5f8d1c6 --- /dev/null +++ b/spec/data/evaluation/options_based_on_custom_attr.json @@ -0,0 +1,31 @@ +{ + "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9e4e-4f59-86b2-5da50924b6ca/08dbc325-9ebd-4587-8171-88f76a3004cb", + "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", + "tests": [ + { + "key": "string75Cat0Dog25Falcon0HorseCustomAttr", + "defaultValue": "default", + "returnValue": "Chicken", + "expectedLog": "options_custom_attribute_no_user.txt" + }, + { + "key": "string75Cat0Dog25Falcon0HorseCustomAttr", + "defaultValue": "default", + "user": { + "Identifier": "12345" + }, + "returnValue": "Chicken", + "expectedLog": "no_options_custom_attribute.txt" + }, + { + "key": "string75Cat0Dog25Falcon0HorseCustomAttr", + "defaultValue": "default", + "user": { + "Identifier": "12345", + "Country": "US" + }, + "returnValue": "Cat", + "expectedLog": "matching_options_custom_attribute.txt" + } + ] +} diff --git a/spec/data/evaluation/options_based_on_custom_attr/matching_options_custom_attribute.txt b/spec/data/evaluation/options_based_on_custom_attr/matching_options_custom_attribute.txt new file mode 100644 index 0000000..2621086 --- /dev/null +++ b/spec/data/evaluation/options_based_on_custom_attr/matching_options_custom_attribute.txt @@ -0,0 +1,5 @@ +INFO [5000] Evaluating 'string75Cat0Dog25Falcon0HorseCustomAttr' for User '{"Identifier":"12345","Country":"US"}' + Evaluating % options based on the User.Country attribute: + - Computing hash in the [0..99] range from User.Country => 70 (this value is sticky and consistent across all SDKs) + - Hash value 70 selects % option 1 (75%), 'Cat'. + Returning 'Cat'. diff --git a/spec/data/evaluation/options_based_on_custom_attr/no_options_custom_attribute.txt b/spec/data/evaluation/options_based_on_custom_attr/no_options_custom_attribute.txt new file mode 100644 index 0000000..de7349c --- /dev/null +++ b/spec/data/evaluation/options_based_on_custom_attr/no_options_custom_attribute.txt @@ -0,0 +1,4 @@ +WARN [3003] Cannot evaluate % options for setting 'string75Cat0Dog25Falcon0HorseCustomAttr' (the User.Country attribute is missing). You should set the User.Country attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'string75Cat0Dog25Falcon0HorseCustomAttr' for User '{"Identifier":"12345"}' + Skipping % options because the User.Country attribute is missing. + Returning 'Chicken'. diff --git a/spec/data/evaluation/options_based_on_custom_attr/options_custom_attribute_no_user.txt b/spec/data/evaluation/options_based_on_custom_attr/options_custom_attribute_no_user.txt new file mode 100644 index 0000000..40ef55b --- /dev/null +++ b/spec/data/evaluation/options_based_on_custom_attr/options_custom_attribute_no_user.txt @@ -0,0 +1,4 @@ +WARN [3001] Cannot evaluate targeting rules and % options for setting 'string75Cat0Dog25Falcon0HorseCustomAttr' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'string75Cat0Dog25Falcon0HorseCustomAttr' + Skipping % options because the User Object is missing. + Returning 'Chicken'. diff --git a/spec/data/evaluation/options_based_on_user_id.json b/spec/data/evaluation/options_based_on_user_id.json new file mode 100644 index 0000000..442f575 --- /dev/null +++ b/spec/data/evaluation/options_based_on_user_id.json @@ -0,0 +1,21 @@ +{ + "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d62463-86ec-8fde-f5b5-1c5c426fc830/244cf8b0-f604-11e8-b543-f23c917f9d8d", + "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", + "tests": [ + { + "key": "string75Cat0Dog25Falcon0Horse", + "defaultValue": "default", + "returnValue": "Chicken", + "expectedLog": "options_user_attribute_no_user.txt" + }, + { + "key": "string75Cat0Dog25Falcon0Horse", + "defaultValue": "default", + "user": { + "Identifier": "12345" + }, + "returnValue": "Cat", + "expectedLog": "options_user_attribute_user.txt" + } + ] +} diff --git a/spec/data/evaluation/options_based_on_user_id/options_user_attribute_no_user.txt b/spec/data/evaluation/options_based_on_user_id/options_user_attribute_no_user.txt new file mode 100644 index 0000000..a36b316 --- /dev/null +++ b/spec/data/evaluation/options_based_on_user_id/options_user_attribute_no_user.txt @@ -0,0 +1,4 @@ +WARN [3001] Cannot evaluate targeting rules and % options for setting 'string75Cat0Dog25Falcon0Horse' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'string75Cat0Dog25Falcon0Horse' + Skipping % options because the User Object is missing. + Returning 'Chicken'. diff --git a/spec/data/evaluation/options_based_on_user_id/options_user_attribute_user.txt b/spec/data/evaluation/options_based_on_user_id/options_user_attribute_user.txt new file mode 100644 index 0000000..dac8dd6 --- /dev/null +++ b/spec/data/evaluation/options_based_on_user_id/options_user_attribute_user.txt @@ -0,0 +1,5 @@ +INFO [5000] Evaluating 'string75Cat0Dog25Falcon0Horse' for User '{"Identifier":"12345"}' + Evaluating % options based on the User.Identifier attribute: + - Computing hash in the [0..99] range from User.Identifier => 21 (this value is sticky and consistent across all SDKs) + - Hash value 21 selects % option 1 (75%), 'Cat'. + Returning 'Cat'. diff --git a/spec/data/evaluation/options_within_targeting_rule.json b/spec/data/evaluation/options_within_targeting_rule.json new file mode 100644 index 0000000..4c6c533 --- /dev/null +++ b/spec/data/evaluation/options_within_targeting_rule.json @@ -0,0 +1,52 @@ +{ + "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9e4e-4f59-86b2-5da50924b6ca/08dbc325-9ebd-4587-8171-88f76a3004cb", + "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", + "tests": [ + { + "key": "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat", + "defaultValue": "default", + "returnValue": "Cat", + "expectedLog": "options_within_targeting_rule_no_user.txt" + }, + { + "key": "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat", + "defaultValue": "default", + "user": { + "Identifier": "12345" + }, + "returnValue": "Cat", + "expectedLog": "options_within_targeting_rule_no_targeted_attribute.txt" + }, + { + "key": "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat", + "defaultValue": "default", + "user": { + "Identifier": "12345", + "Email": "joe@example.com" + }, + "returnValue": "Cat", + "expectedLog": "options_within_targeting_rule_not_matching_targeted_attribute.txt" + }, + { + "key": "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat", + "defaultValue": "default", + "user": { + "Identifier": "12345", + "Email": "joe@configcat.com" + }, + "returnValue": "Cat", + "expectedLog": "options_within_targeting_rule_matching_targeted_attribute_no_options_attribute.txt" + }, + { + "key": "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat", + "defaultValue": "default", + "user": { + "Identifier": "12345", + "Email": "joe@configcat.com", + "Country": "US" + }, + "returnValue": "Cat", + "expectedLog": "options_within_targeting_rule_matching_targeted_attribute_options_attribute.txt" + } + ] +} diff --git a/spec/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_no_options_attribute.txt b/spec/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_no_options_attribute.txt new file mode 100644 index 0000000..edd18e5 --- /dev/null +++ b/spec/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_no_options_attribute.txt @@ -0,0 +1,7 @@ +WARN [3003] Cannot evaluate % options for setting 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' (the User.Country attribute is missing). You should set the User.Country attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' for User '{"Identifier":"12345","Email":"joe@configcat.com"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN % options => MATCH, applying rule + Skipping % options because the User.Country attribute is missing. + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'Cat'. diff --git a/spec/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_options_attribute.txt b/spec/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_options_attribute.txt new file mode 100644 index 0000000..8129521 --- /dev/null +++ b/spec/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_options_attribute.txt @@ -0,0 +1,7 @@ +INFO [5000] Evaluating 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' for User '{"Identifier":"12345","Email":"joe@configcat.com","Country":"US"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN % options => MATCH, applying rule + Evaluating % options based on the User.Country attribute: + - Computing hash in the [0..99] range from User.Country => 63 (this value is sticky and consistent across all SDKs) + - Hash value 63 selects % option 1 (75%), 'Cat'. + Returning 'Cat'. diff --git a/spec/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_no_targeted_attribute.txt b/spec/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_no_targeted_attribute.txt new file mode 100644 index 0000000..990dea1 --- /dev/null +++ b/spec/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_no_targeted_attribute.txt @@ -0,0 +1,6 @@ +WARN [3003] Cannot evaluate condition (User.Email CONTAINS ANY OF ['@configcat.com']) for setting 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' for User '{"Identifier":"12345"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN % options => cannot evaluate, the User.Email attribute is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'Cat'. diff --git a/spec/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_no_user.txt b/spec/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_no_user.txt new file mode 100644 index 0000000..2d69165 --- /dev/null +++ b/spec/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_no_user.txt @@ -0,0 +1,6 @@ +WARN [3001] Cannot evaluate targeting rules and % options for setting 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN % options => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'Cat'. diff --git a/spec/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_not_matching_targeted_attribute.txt b/spec/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_not_matching_targeted_attribute.txt new file mode 100644 index 0000000..dd6032e --- /dev/null +++ b/spec/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_not_matching_targeted_attribute.txt @@ -0,0 +1,4 @@ +INFO [5000] Evaluating 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' for User '{"Identifier":"12345","Email":"joe@example.com"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN % options => no match + Returning 'Cat'. diff --git a/spec/data/evaluation/prerequisite_flag.json b/spec/data/evaluation/prerequisite_flag.json new file mode 100644 index 0000000..9c35c00 --- /dev/null +++ b/spec/data/evaluation/prerequisite_flag.json @@ -0,0 +1,41 @@ +{ + "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9d5e-4988-891c-fd4a45790bd1/08dbc325-9ebd-4587-8171-88f76a3004cb", + "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/ByMO9yZNn02kXcm72lnY1A", + "tests": [ + { + "key": "dependentFeatureWithUserCondition", + "defaultValue": "default", + "returnValue": "Chicken", + "expectedLog": "prerequisite_flag_no_user_needed_by_dep.txt" + }, + { + "key": "dependentFeature", + "defaultValue": "default", + "returnValue": "Chicken", + "expectedLog": "prerequisite_flag_no_user_needed_by_prereq.txt" + }, + { + "key": "dependentFeatureWithUserCondition2", + "defaultValue": "default", + "returnValue": "Frog", + "expectedLog": "prerequisite_flag_no_user_needed_by_both.txt" + }, + { + "key": "dependentFeature", + "defaultValue": "default", + "user": { + "Identifier": "12345", + "Email": "kate@configcat.com", + "Country": "USA" + }, + "returnValue": "Horse", + "expectedLog": "prerequisite_flag.txt" + }, + { + "key": "dependentFeatureMultipleLevels", + "defaultValue": "default", + "returnValue": "Dog", + "expectedLog": "prerequisite_flag_multilevel.txt" + } + ] +} diff --git a/spec/data/evaluation/prerequisite_flag/prerequisite_flag.txt b/spec/data/evaluation/prerequisite_flag/prerequisite_flag.txt new file mode 100644 index 0000000..1d9022b --- /dev/null +++ b/spec/data/evaluation/prerequisite_flag/prerequisite_flag.txt @@ -0,0 +1,32 @@ +INFO [5000] Evaluating 'dependentFeature' for User '{"Identifier":"12345","Email":"kate@configcat.com","Country":"USA"}' + Evaluating targeting rules and applying the first match if any: + - IF Flag 'mainFeature' EQUALS 'target' + ( + Evaluating prerequisite flag 'mainFeature': + Evaluating targeting rules and applying the first match if any: + - IF User.Email ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN 'private' => no match + - IF User.Country IS ONE OF [<1 hashed value>] => true + AND User IS NOT IN SEGMENT 'Beta Users' + ( + Evaluating segment 'Beta Users': + - IF User.Email IS ONE OF [<2 hashed values>] => false, skipping the remaining AND conditions + Segment evaluation result: User IS NOT IN SEGMENT. + Condition (User IS NOT IN SEGMENT 'Beta Users') evaluates to true. + ) => true + AND User IS NOT IN SEGMENT 'Developers' + ( + Evaluating segment 'Developers': + - IF User.Email IS ONE OF [<2 hashed values>] => false, skipping the remaining AND conditions + Segment evaluation result: User IS NOT IN SEGMENT. + Condition (User IS NOT IN SEGMENT 'Developers') evaluates to true. + ) => true + THEN 'target' => MATCH, applying rule + Prerequisite flag evaluation result: 'target'. + Condition (Flag 'mainFeature' EQUALS 'target') evaluates to true. + ) + THEN % options => MATCH, applying rule + Evaluating % options based on the User.Identifier attribute: + - Computing hash in the [0..99] range from User.Identifier => 78 (this value is sticky and consistent across all SDKs) + - Hash value 78 selects % option 4 (25%), 'Horse'. + Returning 'Horse'. diff --git a/spec/data/evaluation/prerequisite_flag/prerequisite_flag_multilevel.txt b/spec/data/evaluation/prerequisite_flag/prerequisite_flag_multilevel.txt new file mode 100644 index 0000000..e9b9da6 --- /dev/null +++ b/spec/data/evaluation/prerequisite_flag/prerequisite_flag_multilevel.txt @@ -0,0 +1,24 @@ +INFO [5000] Evaluating 'dependentFeatureMultipleLevels' + Evaluating targeting rules and applying the first match if any: + - IF Flag 'intermediateFeature' EQUALS 'true' + ( + Evaluating prerequisite flag 'intermediateFeature': + Evaluating targeting rules and applying the first match if any: + - IF Flag 'mainFeatureWithoutUserCondition' EQUALS 'true' + ( + Evaluating prerequisite flag 'mainFeatureWithoutUserCondition': + Prerequisite flag evaluation result: 'true'. + Condition (Flag 'mainFeatureWithoutUserCondition' EQUALS 'true') evaluates to true. + ) => true + AND Flag 'mainFeatureWithoutUserCondition' EQUALS 'true' + ( + Evaluating prerequisite flag 'mainFeatureWithoutUserCondition': + Prerequisite flag evaluation result: 'true'. + Condition (Flag 'mainFeatureWithoutUserCondition' EQUALS 'true') evaluates to true. + ) => true + THEN 'true' => MATCH, applying rule + Prerequisite flag evaluation result: 'true'. + Condition (Flag 'intermediateFeature' EQUALS 'true') evaluates to true. + ) + THEN 'Dog' => MATCH, applying rule + Returning 'Dog'. diff --git a/spec/data/evaluation/prerequisite_flag/prerequisite_flag_no_user_needed_by_both.txt b/spec/data/evaluation/prerequisite_flag/prerequisite_flag_no_user_needed_by_both.txt new file mode 100644 index 0000000..05fa640 --- /dev/null +++ b/spec/data/evaluation/prerequisite_flag/prerequisite_flag_no_user_needed_by_both.txt @@ -0,0 +1,38 @@ +WARN [3001] Cannot evaluate targeting rules and % options for setting 'dependentFeatureWithUserCondition2' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +WARN [3001] Cannot evaluate targeting rules and % options for setting 'mainFeature' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +WARN [3001] Cannot evaluate targeting rules and % options for setting 'mainFeature' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'dependentFeatureWithUserCondition2' + Evaluating targeting rules and applying the first match if any: + - IF User.Email IS ONE OF [<2 hashed values>] THEN 'Dog' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF Flag 'mainFeature' EQUALS 'public' + ( + Evaluating prerequisite flag 'mainFeature': + Evaluating targeting rules and applying the first match if any: + - IF User.Email ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN 'private' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF User.Country IS ONE OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN 'target' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Prerequisite flag evaluation result: 'public'. + Condition (Flag 'mainFeature' EQUALS 'public') evaluates to true. + ) + THEN % options => MATCH, applying rule + Skipping % options because the User Object is missing. + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF Flag 'mainFeature' EQUALS 'public' + ( + Evaluating prerequisite flag 'mainFeature': + Evaluating targeting rules and applying the first match if any: + - IF User.Email ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN 'private' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF User.Country IS ONE OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN 'target' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Prerequisite flag evaluation result: 'public'. + Condition (Flag 'mainFeature' EQUALS 'public') evaluates to true. + ) + THEN 'Frog' => MATCH, applying rule + Returning 'Frog'. diff --git a/spec/data/evaluation/prerequisite_flag/prerequisite_flag_no_user_needed_by_dep.txt b/spec/data/evaluation/prerequisite_flag/prerequisite_flag_no_user_needed_by_dep.txt new file mode 100644 index 0000000..18072fd --- /dev/null +++ b/spec/data/evaluation/prerequisite_flag/prerequisite_flag_no_user_needed_by_dep.txt @@ -0,0 +1,15 @@ +WARN [3001] Cannot evaluate targeting rules and % options for setting 'dependentFeatureWithUserCondition' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'dependentFeatureWithUserCondition' + Evaluating targeting rules and applying the first match if any: + - IF User.Email IS ONE OF [<2 hashed values>] THEN 'Dog' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF Flag 'mainFeatureWithoutUserCondition' EQUALS 'true' + ( + Evaluating prerequisite flag 'mainFeatureWithoutUserCondition': + Prerequisite flag evaluation result: 'true'. + Condition (Flag 'mainFeatureWithoutUserCondition' EQUALS 'true') evaluates to true. + ) + THEN % options => MATCH, applying rule + Skipping % options because the User Object is missing. + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'Chicken'. diff --git a/spec/data/evaluation/prerequisite_flag/prerequisite_flag_no_user_needed_by_prereq.txt b/spec/data/evaluation/prerequisite_flag/prerequisite_flag_no_user_needed_by_prereq.txt new file mode 100644 index 0000000..0d9975a --- /dev/null +++ b/spec/data/evaluation/prerequisite_flag/prerequisite_flag_no_user_needed_by_prereq.txt @@ -0,0 +1,18 @@ +WARN [3001] Cannot evaluate targeting rules and % options for setting 'mainFeature' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'dependentFeature' + Evaluating targeting rules and applying the first match if any: + - IF Flag 'mainFeature' EQUALS 'target' + ( + Evaluating prerequisite flag 'mainFeature': + Evaluating targeting rules and applying the first match if any: + - IF User.Email ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN 'private' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF User.Country IS ONE OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN 'target' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Prerequisite flag evaluation result: 'public'. + Condition (Flag 'mainFeature' EQUALS 'target') evaluates to false. + ) + THEN % options => no match + Returning 'Chicken'. diff --git a/spec/data/evaluation/segment.json b/spec/data/evaluation/segment.json new file mode 100644 index 0000000..1bb4df5 --- /dev/null +++ b/spec/data/evaluation/segment.json @@ -0,0 +1,47 @@ +{ + "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08dbd6ca-a85f-4ed0-888a-2da18def92b5/244cf8b0-f604-11e8-b543-f23c917f9d8d", + "sdkKey": "configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/y_ZB7o-Xb0Swxth-ZlMSeA", + "tests": [ + { + "key": "featureWithSegmentTargeting", + "defaultValue": false, + "returnValue": false, + "expectedLog": "segment_no_user.txt" + }, + { + "key": "featureWithSegmentTargetingMultipleConditions", + "defaultValue": false, + "returnValue": false, + "expectedLog": "segment_no_user_multi_conditions.txt" + }, + { + "key": "featureWithNegatedSegmentTargetingCleartext", + "defaultValue": false, + "user": { + "Identifier": "12345" + }, + "returnValue": false, + "expectedLog": "segment_no_targeted_attribute.txt" + }, + { + "key": "featureWithSegmentTargeting", + "defaultValue": false, + "user": { + "Identifier": "12345", + "Email": "jane@example.com" + }, + "returnValue": true, + "expectedLog": "segment_matching.txt" + }, + { + "key": "featureWithNegatedSegmentTargeting", + "defaultValue": false, + "user": { + "Identifier": "12345", + "Email": "jane@example.com" + }, + "returnValue": false, + "expectedLog": "segment_no_matching.txt" + } + ] +} diff --git a/spec/data/evaluation/segment/segment_matching.txt b/spec/data/evaluation/segment/segment_matching.txt new file mode 100644 index 0000000..9065aae --- /dev/null +++ b/spec/data/evaluation/segment/segment_matching.txt @@ -0,0 +1,11 @@ +INFO [5000] Evaluating 'featureWithSegmentTargeting' for User '{"Identifier":"12345","Email":"jane@example.com"}' + Evaluating targeting rules and applying the first match if any: + - IF User IS IN SEGMENT 'Beta users' + ( + Evaluating segment 'Beta users': + - IF User.Email IS ONE OF [<2 hashed values>] => true + Segment evaluation result: User IS IN SEGMENT. + Condition (User IS IN SEGMENT 'Beta users') evaluates to true. + ) + THEN 'true' => MATCH, applying rule + Returning 'true'. diff --git a/spec/data/evaluation/segment/segment_no_matching.txt b/spec/data/evaluation/segment/segment_no_matching.txt new file mode 100644 index 0000000..0d04d83 --- /dev/null +++ b/spec/data/evaluation/segment/segment_no_matching.txt @@ -0,0 +1,11 @@ +INFO [5000] Evaluating 'featureWithNegatedSegmentTargeting' for User '{"Identifier":"12345","Email":"jane@example.com"}' + Evaluating targeting rules and applying the first match if any: + - IF User IS NOT IN SEGMENT 'Beta users' + ( + Evaluating segment 'Beta users': + - IF User.Email IS ONE OF [<2 hashed values>] => true + Segment evaluation result: User IS IN SEGMENT. + Condition (User IS NOT IN SEGMENT 'Beta users') evaluates to false. + ) + THEN 'true' => no match + Returning 'false'. diff --git a/spec/data/evaluation/segment/segment_no_targeted_attribute.txt b/spec/data/evaluation/segment/segment_no_targeted_attribute.txt new file mode 100644 index 0000000..a83240f --- /dev/null +++ b/spec/data/evaluation/segment/segment_no_targeted_attribute.txt @@ -0,0 +1,13 @@ +WARN [3003] Cannot evaluate condition (User.Email IS ONE OF ['jane@example.com', 'john@example.com']) for setting 'featureWithNegatedSegmentTargetingCleartext' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'featureWithNegatedSegmentTargetingCleartext' for User '{"Identifier":"12345"}' + Evaluating targeting rules and applying the first match if any: + - IF User IS NOT IN SEGMENT 'Beta users (cleartext)' + ( + Evaluating segment 'Beta users (cleartext)': + - IF User.Email IS ONE OF ['jane@example.com', 'john@example.com'] => false, skipping the remaining AND conditions + Segment evaluation result: cannot evaluate, the User.Email attribute is missing. + Condition (User IS NOT IN SEGMENT 'Beta users (cleartext)') failed to evaluate. + ) + THEN 'true' => cannot evaluate, the User.Email attribute is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'false'. diff --git a/spec/data/evaluation/segment/segment_no_user.txt b/spec/data/evaluation/segment/segment_no_user.txt new file mode 100644 index 0000000..8290ef1 --- /dev/null +++ b/spec/data/evaluation/segment/segment_no_user.txt @@ -0,0 +1,6 @@ +WARN [3001] Cannot evaluate targeting rules and % options for setting 'featureWithSegmentTargeting' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'featureWithSegmentTargeting' + Evaluating targeting rules and applying the first match if any: + - IF User IS IN SEGMENT 'Beta users' THEN 'true' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'false'. diff --git a/spec/data/evaluation/segment/segment_no_user_multi_conditions.txt b/spec/data/evaluation/segment/segment_no_user_multi_conditions.txt new file mode 100644 index 0000000..a863af3 --- /dev/null +++ b/spec/data/evaluation/segment/segment_no_user_multi_conditions.txt @@ -0,0 +1,7 @@ +WARN [3001] Cannot evaluate targeting rules and % options for setting 'featureWithSegmentTargetingMultipleConditions' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'featureWithSegmentTargetingMultipleConditions' + Evaluating targeting rules and applying the first match if any: + - IF User IS IN SEGMENT 'Beta users (cleartext)' => false, skipping the remaining AND conditions + THEN 'true' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'false'. diff --git a/spec/data/evaluation/semver_validation.json b/spec/data/evaluation/semver_validation.json new file mode 100644 index 0000000..3a14fc6 --- /dev/null +++ b/spec/data/evaluation/semver_validation.json @@ -0,0 +1,26 @@ +{ + "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d745f1-f315-7daf-d163-5541d3786e6f/244cf8b0-f604-11e8-b543-f23c917f9d8d", + "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/BAr3KgLTP0ObzKnBTo5nhA", + "tests": [ + { + "key": "isNotOneOf", + "defaultValue": "default", + "returnValue": "Default", + "expectedLog": "semver_error.txt", + "user": { + "Identifier": "12345", + "Custom1": "wrong_semver" + } + }, + { + "key": "relations", + "defaultValue": "default", + "returnValue": "Default", + "expectedLog": "semver_relations_error.txt", + "user": { + "Identifier": "12345", + "Custom1": "wrong_semver" + } + } + ] +} diff --git a/spec/data/evaluation/semver_validation/semver_error.txt b/spec/data/evaluation/semver_validation/semver_error.txt new file mode 100644 index 0000000..04d436f --- /dev/null +++ b/spec/data/evaluation/semver_validation/semver_error.txt @@ -0,0 +1,9 @@ +WARN [3004] Cannot evaluate condition (User.Custom1 IS NOT ONE OF ['1.0.0', '1.0.1', '2.0.0', '2.0.1', '2.0.2', '']) for setting 'isNotOneOf' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. +WARN [3004] Cannot evaluate condition (User.Custom1 IS NOT ONE OF ['1.0.0', '3.0.1']) for setting 'isNotOneOf' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. +INFO [5000] Evaluating 'isNotOneOf' for User '{"Identifier":"12345","Custom1":"wrong_semver"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Custom1 IS NOT ONE OF ['1.0.0', '1.0.1', '2.0.0', '2.0.1', '2.0.2', ''] THEN 'Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, )' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF User.Custom1 IS NOT ONE OF ['1.0.0', '3.0.1'] THEN 'Is not one of (1.0.0, 3.0.1)' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'Default'. diff --git a/spec/data/evaluation/semver_validation/semver_relations_error.txt b/spec/data/evaluation/semver_validation/semver_relations_error.txt new file mode 100644 index 0000000..41f49de --- /dev/null +++ b/spec/data/evaluation/semver_validation/semver_relations_error.txt @@ -0,0 +1,18 @@ +WARN [3004] Cannot evaluate condition (User.Custom1 < '1.0.0,') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. +WARN [3004] Cannot evaluate condition (User.Custom1 < '1.0.0') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. +WARN [3004] Cannot evaluate condition (User.Custom1 <= '1.0.0') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. +WARN [3004] Cannot evaluate condition (User.Custom1 > '2.0.0') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. +WARN [3004] Cannot evaluate condition (User.Custom1 >= '2.0.0') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. +INFO [5000] Evaluating 'relations' for User '{"Identifier":"12345","Custom1":"wrong_semver"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Custom1 < '1.0.0,' THEN '<1.0.0,' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF User.Custom1 < '1.0.0' THEN '< 1.0.0' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF User.Custom1 <= '1.0.0' THEN '<=1.0.0' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF User.Custom1 > '2.0.0' THEN '>2.0.0' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF User.Custom1 >= '2.0.0' THEN '>=2.0.0' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'Default'. diff --git a/spec/data/evaluation/simple_value.json b/spec/data/evaluation/simple_value.json new file mode 100644 index 0000000..070d6f5 --- /dev/null +++ b/spec/data/evaluation/simple_value.json @@ -0,0 +1,37 @@ +{ + "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d62463-86ec-8fde-f5b5-1c5c426fc830/244cf8b0-f604-11e8-b543-f23c917f9d8d", + "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", + "tests": [ + { + "key": "boolDefaultFalse", + "defaultValue": true, + "returnValue": false, + "expectedLog": "off_flag.txt" + }, + { + "key": "boolDefaultTrue", + "defaultValue": false, + "returnValue": true, + "expectedLog": "on_flag.txt" + }, + { + "key": "stringDefaultCat", + "defaultValue": "Default", + "returnValue": "Cat", + "expectedLog": "text_setting.txt" + }, + { + "key": "integerDefaultOne", + "defaultValue": 0, + "returnValue": 1, + "expectedLog": "int_setting.txt" + }, + { + "testName": "double_setting", + "key": "doubleDefaultPi", + "defaultValue": 0.0, + "returnValue": 3.1415, + "expectedLog": "double_setting.txt" + } + ] +} diff --git a/spec/data/evaluation/simple_value/double_setting.txt b/spec/data/evaluation/simple_value/double_setting.txt new file mode 100644 index 0000000..4a632f7 --- /dev/null +++ b/spec/data/evaluation/simple_value/double_setting.txt @@ -0,0 +1,2 @@ +INFO [5000] Evaluating 'doubleDefaultPi' + Returning '3.1415'. diff --git a/spec/data/evaluation/simple_value/int_setting.txt b/spec/data/evaluation/simple_value/int_setting.txt new file mode 100644 index 0000000..1361843 --- /dev/null +++ b/spec/data/evaluation/simple_value/int_setting.txt @@ -0,0 +1,2 @@ +INFO [5000] Evaluating 'integerDefaultOne' + Returning '1'. diff --git a/spec/data/evaluation/simple_value/off_flag.txt b/spec/data/evaluation/simple_value/off_flag.txt new file mode 100644 index 0000000..4580685 --- /dev/null +++ b/spec/data/evaluation/simple_value/off_flag.txt @@ -0,0 +1,2 @@ +INFO [5000] Evaluating 'boolDefaultFalse' + Returning 'false'. diff --git a/spec/data/evaluation/simple_value/on_flag.txt b/spec/data/evaluation/simple_value/on_flag.txt new file mode 100644 index 0000000..274c990 --- /dev/null +++ b/spec/data/evaluation/simple_value/on_flag.txt @@ -0,0 +1,2 @@ +INFO [5000] Evaluating 'boolDefaultTrue' + Returning 'true'. diff --git a/spec/data/evaluation/simple_value/text_setting.txt b/spec/data/evaluation/simple_value/text_setting.txt new file mode 100644 index 0000000..831d7c6 --- /dev/null +++ b/spec/data/evaluation/simple_value/text_setting.txt @@ -0,0 +1,2 @@ +INFO [5000] Evaluating 'stringDefaultCat' + Returning 'Cat'. diff --git a/spec/test-simple.json b/spec/data/test-simple.json similarity index 100% rename from spec/test-simple.json rename to spec/data/test-simple.json diff --git a/spec/data/test.json b/spec/data/test.json new file mode 100644 index 0000000..63c93c7 --- /dev/null +++ b/spec/data/test.json @@ -0,0 +1,24 @@ +{ + "f": { + "disabledFeature": { + "t": 0, + "v": { "b": false } + }, + "enabledFeature": { + "t": 0, + "v": { "b": true } + }, + "intSetting": { + "t": 2, + "v": { "i": 5 } + }, + "doubleSetting": { + "t": 3, + "v": { "d": 3.14 } + }, + "stringSetting": { + "t": 1, + "v": { "s": "test" } + } + } +} \ No newline at end of file diff --git a/spec/data/test_circulardependency_v6.json b/spec/data/test_circulardependency_v6.json new file mode 100644 index 0000000..a8a9e17 --- /dev/null +++ b/spec/data/test_circulardependency_v6.json @@ -0,0 +1,80 @@ +{ + "p": { + "u": "https://cdn-global.configcat.com", + "r": 0 + }, + "f": { + "key1": { + "t": 1, + "v": { "s": "key1-value" }, + "r": [ + { + "c": [ + { + "p": { + "f": "key1", + "c": 0, + "v": { "s": "key1-prereq" } + } + } + ], + "s": { "v": { "s": "key1-prereq" } } + } + ] + }, + "key2": { + "t": 1, + "v": { "s": "key2-value" }, + "r": [ + { + "c": [ + { + "p": { + "f": "key3", + "c": 0, + "v": { "s": "key3-prereq" } + } + } + ], + "s": { "v": { "s": "key2-prereq" } } + } + ] + }, + "key3": { + "t": 1, + "v": { "s": "key3-value" }, + "r": [ + { + "c": [ + { + "p": { + "f": "key2", + "c": 0, + "v": { "s": "key2-prereq" } + } + } + ], + "s": { "v": { "s": "key3-prereq" } } + } + ] + }, + "key4": { + "t": 1, + "v": { "s": "key4-value" }, + "r": [ + { + "c": [ + { + "p": { + "f": "key3", + "c": 0, + "v": { "s": "key3-prereq" } + } + } + ], + "s": { "v": { "s": "key4-prereq" } } + } + ] + } + } +} diff --git a/spec/data/test_override_flagdependency_v6.json b/spec/data/test_override_flagdependency_v6.json new file mode 100644 index 0000000..62e159e --- /dev/null +++ b/spec/data/test_override_flagdependency_v6.json @@ -0,0 +1,44 @@ +{ + "p": { + "u": "https://test-cdn-eu.configcat.com", + "r": 0, + "s": "TsTuRHo\u002BMHs8h8j16HQY83sooJsLg34Ir5KIVOletFU=" + }, + "f": { + "mainStringFlag": { + "t": 1, + "v": { + "s": "private" + }, + "i": "24c96275" + }, + "stringDependsOnInt": { + "t": 1, + "r": [ + { + "c": [ + { + "p": { + "f": "mainIntFlag", + "c": 0, + "v": { + "i": 42 + } + } + } + ], + "s": { + "v": { + "s": "Dog" + }, + "i": "12531eec" + } + } + ], + "v": { + "s": "Cat" + }, + "i": "e227d926" + } + } +} diff --git a/spec/data/test_override_segments_v6.json b/spec/data/test_override_segments_v6.json new file mode 100644 index 0000000..47bf15c --- /dev/null +++ b/spec/data/test_override_segments_v6.json @@ -0,0 +1,66 @@ +{ + "p": { + "u": "https://test-cdn-eu.configcat.com", + "r": 0, + "s": "80xCU/SlDz1lCiWFaxIBjyJeJecWjq46T4eu6GtozkM=" + }, + "s": [ + { + "n": "Beta Users", + "r": [ + { + "a": "Email", + "c": 16, + "l": [ + "9189c42f6035bd1d2df5eda347a4f62926d27c80540a7aa6cc72cc75bc6757ff" + ] + } + ] + }, + { + "n": "Developers", + "r": [ + { + "a": "Email", + "c": 16, + "l": [ + "a7cdf54e74b5527bd2617889ec47f6d29b825ccfc97ff00832886bcb735abded" + ] + } + ] + } + ], + "f": { + "developerAndBetaUserSegment": { + "t": 0, + "r": [ + { + "c": [ + { + "s": { + "s": 1, + "c": 0 + } + }, + { + "s": { + "s": 0, + "c": 1 + } + } + ], + "s": { + "v": { + "b": true + }, + "i": "ddc50638" + } + } + ], + "v": { + "b": false + }, + "i": "6427f4b8" + } + } +} diff --git a/spec/testmatrix.csv b/spec/data/testmatrix.csv similarity index 100% rename from spec/testmatrix.csv rename to spec/data/testmatrix.csv diff --git a/spec/data/testmatrix_and_or.csv b/spec/data/testmatrix_and_or.csv new file mode 100644 index 0000000..5a149f4 --- /dev/null +++ b/spec/data/testmatrix_and_or.csv @@ -0,0 +1,15 @@ +Identifier;Email;Country;Custom1;mainFeature;dependentFeature;emailAnd;emailOr +##null##;;;;public;Chicken;Cat;Cat +;;;;public;Chicken;Cat;Cat +jane@example.com;jane@example.com;##null##;##null##;public;Chicken;Cat;Jane +john@example.com;john@example.com;##null##;##null##;public;Chicken;Cat;John +a@example.com;a@example.com;USA;##null##;target;Cat;Cat;Cat +mark@example.com;mark@example.com;USA;##null##;target;Dog;Cat;Mark +nora@example.com;nora@example.com;USA;##null##;target;Falcon;Cat;Cat +stern@msn.com;stern@msn.com;USA;##null##;target;Horse;Cat;Cat +jane@sensitivecompany.com;jane@sensitivecompany.com;England;##null##;private;Chicken;Dog;Jane +anna@sensitivecompany.com;anna@sensitivecompany.com;France;##null##;private;Chicken;Cat;Cat +jane@sensitivecompany.com;jane@sensitivecompany.com;england;##null##;public;Chicken;Dog;Jane +jane;jane;##null##;##null##;public;Chicken;Cat;Cat +@sensitivecompany.com;@sensitivecompany.com;##null##;##null##;public;Chicken;Cat;Cat +jane.sensitivecompany.com;jane.sensitivecompany.com;##null##;##null##;public;Chicken;Cat;Cat diff --git a/spec/data/testmatrix_comparators_v6.csv b/spec/data/testmatrix_comparators_v6.csv new file mode 100644 index 0000000..5258feb --- /dev/null +++ b/spec/data/testmatrix_comparators_v6.csv @@ -0,0 +1,24 @@ +Identifier;Email;Country;Custom1;boolTrueIn202304;stringEqualsDogDefaultCat;stringEqualsCleartextDogDefaultCat;stringDoseNotEqualDogDefaultCat;stringNotEqualsCleartextDogDefaultCat;stringStartsWithDogDefaultCat;stringNotStartsWithDogDefaultCat;stringEndsWithDogDefaultCat;stringNotEndsWithDogDefaultCat;arrayContainsDogDefaultCat;arrayDoesNotContainDogDefaultCat;arrayContainsCaseCheckDogDefaultCat;arrayDoesNotContainCaseCheckDogDefaultCat;customPercentageAttribute;missingPercentageAttribute;countryPercentageAttribute;stringContainsAnyOfDogDefaultCat;stringNotContainsAnyOfDogDefaultCat;stringStartsWithAnyOfDogDefaultCat;stringStartsWithAnyOfCleartextDogDefaultCat;stringNotStartsWithAnyOfDogDefaultCat;stringNotStartsWithAnyOfCleartextDogDefaultCat;stringEndsWithAnyOfDogDefaultCat;stringEndsWithAnyOfCleartextDogDefaultCat;stringNotEndsWithAnyOfDogDefaultCat;stringNotEndsWithAnyOfCleartextDogDefaultCat;stringArrayContainsAnyOfDogDefaultCat;stringArrayContainsAnyOfCleartextDogDefaultCat;stringArrayNotContainsAnyOfDogDefaultCat;stringArrayNotContainsAnyOfCleartextDogDefaultCat +##null##;;;;false;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Chicken;Chicken;Chicken;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +;;;;false;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Chicken;Chicken;Chicken;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +a@configcat.com;a@configcat.com;##null##;##null##;false;Dog;Dog;Dog;Dog;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Chicken;NotFound;Chicken;Cat;Dog;Dog;Dog;Cat;Cat;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat +b@configcat.com;b@configcat.com;Hungary;0;false;Cat;Cat;Cat;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Horse;NotFound;Falcon;Cat;Dog;Dog;Dog;Cat;Cat;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat +c@configcat.com;c@configcat.com;United Kingdom;1680307199.9;false;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Falcon;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat +anna@configcat.com;anna@configcat.com;Hungary;1681118000.56;true;Cat;Cat;Dog;Dog;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Falcon;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat +bogjobber@verizon.net;bogjobber@verizon.net;##null##;1682899200.1;false;Cat;Cat;Dog;Dog;Cat;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Horse;Chicken;Chicken;Dog;Cat;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +cliffordj@aol.com;cliffordj@aol.com;Austria;1682999200;false;Cat;Cat;Dog;Dog;Cat;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Falcon;Chicken;Falcon;Dog;Cat;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +reader@configcat.com;reader@configcat.com;Bahamas;read,execute;false;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Falcon;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat +writer@configcat.com;writer@configcat.com;Belgium;write, execute;false;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Horse;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat +reader@configcat.com;reader@configcat.com;Canada;execute, Read;false;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Horse;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat +writer@configcat.com;writer@configcat.com;China;Write;false;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat +admin@configcat.com;admin@configcat.com;France;read, write,execute;false;Cat;Cat;Dog;Dog;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat +user@configcat.com;user@configcat.com;Greece;,execute;false;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat +reader@configcat.com;reader@configcat.com;Bahamas;["read","execute"];false;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Dog;Falcon;NotFound;Falcon;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat +writer@configcat.com;writer@configcat.com;Belgium;["write", "execute"];false;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Dog;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat +reader@configcat.com;reader@configcat.com;Canada;["execute", "Read"];false;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Horse;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat +writer@configcat.com;writer@configcat.com;China;["Write"];false;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Cat;Cat;Horse;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog +admin@configcat.com;admin@configcat.com;France;["read", "write","execute"];false;Cat;Cat;Dog;Dog;Dog;Cat;Dog;Cat;Dog;Cat;Cat;Dog;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat +admin@configcat.com;admin@configcat.com;France;["Read", "Write", "execute"];false;Cat;Cat;Dog;Dog;Dog;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Horse;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat +admin@configcat.com;admin@configcat.com;France;["Read", "Write", "eXecute"];false;Cat;Cat;Dog;Dog;Dog;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog +user@configcat.com;user@configcat.com;Greece;["","execute"];false;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Cat;Dog;Horse;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat +user@configcat.com;user@configcat.com;Monaco;,null, ,,nil, None;false;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat diff --git a/spec/testmatrix_number.csv b/spec/data/testmatrix_number.csv similarity index 100% rename from spec/testmatrix_number.csv rename to spec/data/testmatrix_number.csv diff --git a/spec/data/testmatrix_prerequisite_flag.csv b/spec/data/testmatrix_prerequisite_flag.csv new file mode 100644 index 0000000..74012d3 --- /dev/null +++ b/spec/data/testmatrix_prerequisite_flag.csv @@ -0,0 +1,5 @@ +Identifier;Email;Country;Custom1;mainBoolFlag;mainStringFlag;mainIntFlag;mainDoubleFlag;stringDependsOnBool;stringDependsOnString;stringDependsOnStringCaseCheck;stringDependsOnInt;stringDependsOnDouble;stringDependsOnDoubleIntValue;boolDependsOnBool;intDependsOnBool;doubleDependsOnBool;boolDependsOnBoolDependsOnBool;mainBoolFlagEmpty;stringDependsOnEmptyBool;stringInverseDependsOnEmptyBool;mainBoolFlagInverse;boolDependsOnBoolInverse +##null##;;;;true;public;42;3.14;Dog;Cat;Cat;Cat;Cat;Cat;true;1;1.1;false;true;EmptyOn;EmptyOn;false;true +;;;;true;public;42;3.14;Dog;Cat;Cat;Cat;Cat;Cat;true;1;1.1;false;true;EmptyOn;EmptyOn;false;true +john@sensitivecompany.com;john@sensitivecompany.com;##null##;##null##;false;private;2;0.1;Cat;Dog;Cat;Dog;Dog;Cat;false;42;3.14;true;true;EmptyOn;EmptyOn;true;false +jane@example.com;jane@example.com;##null##;##null##;true;public;42;3.14;Dog;Cat;Cat;Cat;Cat;Cat;true;1;1.1;false;true;EmptyOn;EmptyOn;false;true diff --git a/spec/data/testmatrix_segments.csv b/spec/data/testmatrix_segments.csv new file mode 100644 index 0000000..2c6eab8 --- /dev/null +++ b/spec/data/testmatrix_segments.csv @@ -0,0 +1,6 @@ +Identifier;Email;Country;Custom1;developerAndBetaUserSegment;developerAndBetaUserCleartextSegment;notDeveloperAndNotBetaUserSegment;notDeveloperAndNotBetaUserCleartextSegment +##null##;;;;false;false;false;false +;;;;false;false;false;false +john@example.com;john@example.com;##null##;##null##;false;false;false;false +jane@example.com;jane@example.com;##null##;##null##;false;false;false;false +kate@example.com;kate@example.com;##null##;##null##;true;true;true;true diff --git a/spec/data/testmatrix_segments_old.csv b/spec/data/testmatrix_segments_old.csv new file mode 100644 index 0000000..02aa5a2 --- /dev/null +++ b/spec/data/testmatrix_segments_old.csv @@ -0,0 +1,6 @@ +Identifier;Email;Country;Custom1;featureWithSegmentTargeting;featureWithSegmentTargetingCleartext;featureWithNegatedSegmentTargeting;featureWithNegatedSegmentTargetingCleartext;featureWithSegmentTargetingInverse;featureWithSegmentTargetingInverseCleartext;featureWithNegatedSegmentTargetingInverse;featureWithNegatedSegmentTargetingInverseCleartext +##null##;;;;false;false;false;false;false;false;false;false +;;;;false;false;false;false;false;false;false;false +john@example.com;john@example.com;##null##;##null##;true;true;false;false;false;false;true;true +jane@example.com;jane@example.com;##null##;##null##;true;true;false;false;false;false;true;true +kate@example.com;kate@example.com;##null##;##null##;false;false;true;true;true;true;false;false diff --git a/spec/testmatrix_semantic.csv b/spec/data/testmatrix_semantic.csv similarity index 100% rename from spec/testmatrix_semantic.csv rename to spec/data/testmatrix_semantic.csv diff --git a/spec/testmatrix_semantic_2.csv b/spec/data/testmatrix_semantic_2.csv similarity index 100% rename from spec/testmatrix_semantic_2.csv rename to spec/data/testmatrix_semantic_2.csv diff --git a/spec/testmatrix_sensitive.csv b/spec/data/testmatrix_sensitive.csv similarity index 100% rename from spec/testmatrix_sensitive.csv rename to spec/data/testmatrix_sensitive.csv diff --git a/spec/data/testmatrix_unicode.csv b/spec/data/testmatrix_unicode.csv new file mode 100644 index 0000000..aff2b75 --- /dev/null +++ b/spec/data/testmatrix_unicode.csv @@ -0,0 +1,14 @@ +Identifier;Email;Country;🆃🅴🆇🆃;boolTextEqualsHashed;boolTextEqualsCleartext;boolTextNotEqualsHashed;boolTextNotEqualsCleartext;boolIsOneOfHashed;boolIsOneOfCleartext;boolIsNotOneOfHashed;boolIsNotOneOfCleartext;boolStartsWithHashed;boolStartsWithCleartext;boolNotStartsWithHashed;boolNotStartsWithCleartext;boolEndsWithHashed;boolEndsWithCleartext;boolNotEndsWithHashed;boolNotEndsWithCleartext;boolContainsCleartext;boolNotContainsCleartext;boolArrayContainsHashed;boolArrayContainsCleartext;boolArrayNotContainsHashed;boolArrayNotContainsCleartext +1;;;ʄǟռƈʏ ȶɛӼȶ;true;true;false;false;false;false;true;true;false;false;true;true;false;false;true;true;false;true;false;false;false;false +1;;;ʄaռƈʏ ȶɛӼȶ;false;false;true;true;false;false;true;true;false;false;true;true;false;false;true;true;false;true;false;false;false;false +1;;;ÁRVÍZTŰRŐ tükörfúrógép;false;false;true;true;true;true;false;false;true;true;false;false;true;true;false;false;true;false;false;false;false;false +1;;;árvíztűrő tükörfúrógép;false;false;true;true;false;false;true;true;false;false;true;true;true;true;false;false;true;false;false;false;false;false +1;;;ÁRVÍZTŰRŐ TÜKÖRFÚRÓGÉP;false;false;true;true;false;false;true;true;true;true;false;false;false;false;true;true;true;false;false;false;false;false +1;;;árvíztűrő TÜKÖRFÚRÓGÉP;false;false;true;true;false;false;true;true;false;false;true;true;false;false;true;true;false;true;false;false;false;false +1;;;u𝖓𝖎𝖈𝖔𝖉e;false;false;true;true;true;true;false;false;true;true;false;false;true;true;false;false;true;false;false;false;false;false +;;;𝖚𝖓𝖎𝖈𝖔𝖉e;false;false;true;true;false;false;true;true;false;false;true;true;true;true;false;false;true;false;false;false;false;false +;;;u𝖓𝖎𝖈𝖔𝖉𝖊;false;false;true;true;false;false;true;true;true;true;false;false;false;false;true;true;true;false;false;false;false;false +;;;𝖚𝖓𝖎𝖈𝖔𝖉𝖊;false;false;true;true;false;false;true;true;false;false;true;true;false;false;true;true;false;true;false;false;false;false +1;;;["ÁRVÍZTŰRŐ tükörfúrógép", "unicode"];false;false;true;true;false;false;true;true;false;false;true;true;false;false;true;true;true;false;true;true;false;false +1;;;["ÁRVÍZTŰRŐ", "tükörfúrógép", "u𝖓𝖎𝖈𝖔𝖉e"];false;false;true;true;false;false;true;true;false;false;true;true;false;false;true;true;true;false;true;true;false;false +1;;;["ÁRVÍZTŰRŐ", "tükörfúrógép", "unicode"];false;false;true;true;false;false;true;true;false;false;true;true;false;false;true;true;true;false;false;false;true;true diff --git a/spec/testmatrix_variationId.csv b/spec/data/testmatrix_variationId.csv similarity index 100% rename from spec/testmatrix_variationId.csv rename to spec/data/testmatrix_variationId.csv diff --git a/spec/evaluationlog_spec.rb b/spec/evaluationlog_spec.rb new file mode 100644 index 0000000..672a2ef --- /dev/null +++ b/spec/evaluationlog_spec.rb @@ -0,0 +1,137 @@ +require 'spec_helper' +require_relative 'configcat/mocks' + +RSpec.describe 'Evaluation log test', type: :feature do + + it "test_simple_value" do + expect(test_evaluation_log("./data/evaluation/simple_value.json")).to be true + end + + it "test_1_targeting_rule" do + expect(test_evaluation_log("./data/evaluation/1_targeting_rule.json")).to be true + end + + it "test_2_targeting_rules" do + expect(test_evaluation_log("./data/evaluation/2_targeting_rules.json")).to be true + end + + it "test_options_based_on_user_id" do + expect(test_evaluation_log("./data/evaluation/options_based_on_user_id.json")).to be true + end + + it "test_options_based_on_custom_attr" do + expect(test_evaluation_log("./data/evaluation/options_based_on_custom_attr.json")).to be true + end + + it "test_options_after_targeting_rule" do + expect(test_evaluation_log("./data/evaluation/options_after_targeting_rule.json")).to be true + end + + it "test_options_within_targeting_rule" do + expect(test_evaluation_log("./data/evaluation/options_within_targeting_rule.json")).to be true + end + + it "test_and_rules" do + expect(test_evaluation_log("./data/evaluation/and_rules.json")).to be true + end + + it "test_segment" do + expect(test_evaluation_log("./data/evaluation/segment.json")).to be true + end + + it "test_prerequisite_flag" do + expect(test_evaluation_log("./data/evaluation/prerequisite_flag.json")).to be true + end + + it "test_semver_validation" do + expect(test_evaluation_log("./data/evaluation/semver_validation.json")).to be true + end + + it "test_epoch_date_validation" do + expect(test_evaluation_log("./data/evaluation/epoch_date_validation.json")).to be true + end + + it "test_number_validation" do + expect(test_evaluation_log("./data/evaluation/number_validation.json")).to be true + end + + it "test_comparators_validation" do + expect(test_evaluation_log("./data/evaluation/comparators.json")).to be true + end + + it "test_list_truncation_validation" do + expect(test_evaluation_log("./data/evaluation/list_truncation.json")).to be true + end + + def test_evaluation_log(file_path) + script_dir = File.dirname(__FILE__) + full_file_path = File.join(script_dir, file_path) + expect(File.file?(full_file_path)).to be true + + name = File.basename(file_path, '.json') + file_dir = File.join(File.dirname(full_file_path), name) + + data = JSON.parse(File.read(full_file_path)) + sdk_key = data['sdkKey'] + base_url = data['baseUrl'] + json_override = data['jsonOverride'] + flag_overrides = nil + if json_override + flag_overrides = LocalFileFlagOverrides.new(File.join(file_dir, json_override), OverrideBehaviour::LOCAL_ONLY) + sdk_key ||= TEST_SDK_KEY + end + + begin + # Setup logging + logger = ConfigCat.logger + log_stream = StringIO.new + ConfigCat.logger = Logger.new(log_stream, level: Logger::INFO, formatter: proc do |severity, datetime, progname, msg| + "#{severity} #{msg}\n" + end) + + client = ConfigCatClient.get(sdk_key, ConfigCatOptions.new(polling_mode: PollingMode.manual_poll, + flag_overrides: flag_overrides, + base_url: base_url)) + client.force_refresh + + data['tests'].each do |test| + key = test['key'] + default_value = test['defaultValue'] + return_value = test['returnValue'] + user_data = test['user'] + expected_log_file = test['expectedLog'] + test_name = expected_log_file.sub('.json', '') + + expected_log_file_path = File.join(file_dir, expected_log_file) + user_object = nil + if user_data + identifier = user_data['Identifier'] + email = user_data['Email'] + country = user_data['Country'] + custom = user_data.reject { |k, _| ['Identifier', 'Email', 'Country'].include?(k) } + custom = nil if custom.empty? + user_object = User.new(identifier, email: email, country: country, custom: custom) + end + + # Clear log + log_stream.reopen("") + + value = client.get_value(key, default_value, user_object) + log_stream.rewind + log = log_stream.read + + expect(File.file?(expected_log_file_path)).to be true + expected_log = File.read(expected_log_file_path) + + # Compare logs and values + expect(expected_log.strip).to eq(log.strip), "Log mismatch for test: #{test_name}" + expect(return_value).to eq(value), "Return value mismatch for test: #{test_name}" + end + + return true + ensure + client.close + ConfigCat.logger = logger + end + end +end diff --git a/spec/integration_spec.rb b/spec/integration_spec.rb index 81b2ca8..7ae79a1 100644 --- a/spec/integration_spec.rb +++ b/spec/integration_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -_SDK_KEY = "PKDVCLf-Hq-h-kCzMp-L7Q/PaDVCFk9EpmD6sLpGLltTA" +_SDK_KEY = "configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/1cGEJXUwYUGZCBOL-E2sOw" RSpec.describe 'Integration test: DefaultTests', type: :feature do it "test_without_sdk_key" do expect { @@ -19,6 +19,7 @@ keys = client.get_all_keys expect(keys.size).to eq 5 expect(keys).to include "keySampleText" + client.close end it "test_force_refresh" do diff --git a/spec/override_spec.rb b/spec/override_spec.rb index 04ca1b9..9c7fd86 100644 --- a/spec/override_spec.rb +++ b/spec/override_spec.rb @@ -3,33 +3,25 @@ require 'configcat/localfiledatasource' require 'tempfile' require 'json' +require_relative 'configcat/mocks' + RSpec.describe 'Override test', type: :feature do script_dir = File.dirname(__FILE__) def stub_request - uri_template = Addressable::Template.new "https://{base_url}/{base_path}/{api_key}/{base_ext}" - json = '{"f": {"fakeKey": {"v": false} } }' - WebMock.stub_request(:get, uri_template) - .with( - body: "", - headers: { - 'Accept' => '*/*', - 'Content-Type' => 'application/json', - 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3' - } - ) - .to_return(status: 200, body: json, headers: {}) + json = '{"f": {"fakeKey": {"v": {"b": false}, "t": 0}, "fakeKey2": {"v": {"s": "test"}, "t": 1}}}' + WebMock.stub_request(:get, Regexp.new('https://.*')).to_return(status: 200, body: json, headers: {}) end it "test file" do options = ConfigCat::ConfigCatOptions.new(polling_mode: ConfigCat::PollingMode.manual_poll, flag_overrides: ConfigCat::LocalFileFlagOverrides.new( - File.join(script_dir, "test.json"), + File.join(script_dir, "data/test.json"), ConfigCat::OverrideBehaviour::LOCAL_ONLY ) ) - client = ConfigCat::ConfigCatClient.get("test", options) + client = ConfigCat::ConfigCatClient.get("", options) expect(client.get_value("enabledFeature", false)).to eq true expect(client.get_value("disabledFeature", true)).to eq false @@ -42,11 +34,11 @@ def stub_request it "test simple file" do options = ConfigCat::ConfigCatOptions.new(polling_mode: ConfigCat::PollingMode.manual_poll, flag_overrides: ConfigCat::LocalFileFlagOverrides.new( - File.join(script_dir, "test-simple.json"), + File.join(script_dir, "data/test-simple.json"), ConfigCat::OverrideBehaviour::LOCAL_ONLY ) ) - client = ConfigCat::ConfigCatClient.get("test", options) + client = ConfigCat::ConfigCatClient.get(TEST_SDK_KEY, options) expect(client.get_value("enabledFeature", false)).to eq true expect(client.get_value("disabledFeature", true)).to eq false @@ -63,7 +55,7 @@ def stub_request ConfigCat::OverrideBehaviour::LOCAL_ONLY ) ) - client = ConfigCat::ConfigCatClient.get("test", options) + client = ConfigCat::ConfigCatClient.get(TEST_SDK_KEY, options) expect(client.get_value("enabledFeature", false)).to eq false client.close @@ -82,7 +74,7 @@ def stub_request ConfigCat::OverrideBehaviour::LOCAL_ONLY ) ) - client = ConfigCat::ConfigCatClient.get("test", options) + client = ConfigCat::ConfigCatClient.get(TEST_SDK_KEY, options) expect(client.get_value("enabledFeature", true)).to eq false @@ -117,7 +109,7 @@ def stub_request ConfigCat::OverrideBehaviour::LOCAL_ONLY ) ) - client = ConfigCat::ConfigCatClient.get("test", options) + client = ConfigCat::ConfigCatClient.get(TEST_SDK_KEY, options) expect(client.get_value("enabledFeature", false)).to eq false client.close @@ -140,7 +132,7 @@ def stub_request ConfigCat::OverrideBehaviour::LOCAL_ONLY ) ) - client = ConfigCat::ConfigCatClient.get("test", options) + client = ConfigCat::ConfigCatClient.get(TEST_SDK_KEY, options) expect(client.get_value("enabledFeature", false)).to eq true expect(client.get_value("disabledFeature", true)).to eq false @@ -162,7 +154,7 @@ def stub_request ConfigCat::OverrideBehaviour::LOCAL_OVER_REMOTE ) ) - client = ConfigCat::ConfigCatClient.get("test", options) + client = ConfigCat::ConfigCatClient.get(TEST_SDK_KEY, options) client.force_refresh expect(client.get_value("fakeKey", false)).to eq true @@ -183,7 +175,7 @@ def stub_request ConfigCat::OverrideBehaviour::REMOTE_OVER_LOCAL ) ) - client = ConfigCat::ConfigCatClient.get("test", options) + client = ConfigCat::ConfigCatClient.get(TEST_SDK_KEY, options) client.force_refresh expect(client.get_value("fakeKey", true)).to eq false @@ -191,4 +183,71 @@ def stub_request client.close end + + [ + ['stringDependsOnString', '1', 'john@sensitivecompany.com', nil, 'Dog'], + ['stringDependsOnString', '1', 'john@sensitivecompany.com', OverrideBehaviour::REMOTE_OVER_LOCAL, 'Dog'], + ['stringDependsOnString', '1', 'john@sensitivecompany.com', OverrideBehaviour::LOCAL_OVER_REMOTE, 'Dog'], + ['stringDependsOnString', '1', 'john@sensitivecompany.com', OverrideBehaviour::LOCAL_ONLY, nil], + ['stringDependsOnString', '2', 'john@notsensitivecompany.com', nil, 'Cat'], + ['stringDependsOnString', '2', 'john@notsensitivecompany.com', OverrideBehaviour::REMOTE_OVER_LOCAL, 'Cat'], + ['stringDependsOnString', '2', 'john@notsensitivecompany.com', OverrideBehaviour::LOCAL_OVER_REMOTE, 'Dog'], + ['stringDependsOnString', '2', 'john@notsensitivecompany.com', OverrideBehaviour::LOCAL_ONLY, nil], + ['stringDependsOnInt', '1', 'john@sensitivecompany.com', nil, 'Dog'], + ['stringDependsOnInt', '1', 'john@sensitivecompany.com', OverrideBehaviour::REMOTE_OVER_LOCAL, 'Dog'], + ['stringDependsOnInt', '1', 'john@sensitivecompany.com', OverrideBehaviour::LOCAL_OVER_REMOTE, 'Cat'], + ['stringDependsOnInt', '1', 'john@sensitivecompany.com', OverrideBehaviour::LOCAL_ONLY, nil], + ['stringDependsOnInt', '2', 'john@notsensitivecompany.com', nil, 'Cat'], + ['stringDependsOnInt', '2', 'john@notsensitivecompany.com', OverrideBehaviour::REMOTE_OVER_LOCAL, 'Cat'], + ['stringDependsOnInt', '2', 'john@notsensitivecompany.com', OverrideBehaviour::LOCAL_OVER_REMOTE, 'Dog'], + ['stringDependsOnInt', '2', 'john@notsensitivecompany.com', OverrideBehaviour::LOCAL_ONLY, nil] + ].each do |key, user_id, email, override_behaviour, expected_value| + it "test prerequisite flag override (#{key}, #{user_id}, #{email}, #{override_behaviour}, #{expected_value})" do + # The flag override alters the definition of the following flags: + # * 'mainStringFlag': to check the case where a prerequisite flag is overridden (dependent flag: 'stringDependsOnString') + # * 'stringDependsOnInt': to check the case where a dependent flag is overridden (prerequisite flag: 'mainIntFlag') + options = ConfigCatOptions.new( + polling_mode: PollingMode.manual_poll, + flag_overrides: override_behaviour.nil? ? nil : LocalFileFlagOverrides.new( + File.join(script_dir, "data/test_override_flagdependency_v6.json"), override_behaviour + ) + ) + client = ConfigCatClient.get('configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/JoGwdqJZQ0K2xDy7LnbyOg', options) + client.force_refresh + value = client.get_value(key, nil, User.new(user_id, email: email)) + + expect(value).to eq(expected_value) + client.close + end + end + + [ + ['developerAndBetaUserSegment', '1', 'john@example.com', nil, false], + ['developerAndBetaUserSegment', '1', 'john@example.com', OverrideBehaviour::REMOTE_OVER_LOCAL, false], + ['developerAndBetaUserSegment', '1', 'john@example.com', OverrideBehaviour::LOCAL_OVER_REMOTE, true], + ['developerAndBetaUserSegment', '1', 'john@example.com', OverrideBehaviour::LOCAL_ONLY, true], + ['notDeveloperAndNotBetaUserSegment', '2', 'kate@example.com', nil, true], + ['notDeveloperAndNotBetaUserSegment', '2', 'kate@example.com', OverrideBehaviour::REMOTE_OVER_LOCAL, true], + ['notDeveloperAndNotBetaUserSegment', '2', 'kate@example.com', OverrideBehaviour::LOCAL_OVER_REMOTE, true], + ['notDeveloperAndNotBetaUserSegment', '2', 'kate@example.com', OverrideBehaviour::LOCAL_ONLY, nil] + ].each do |key, user_id, email, override_behaviour, expected_value| + it "test config salt segment override (#{key}, #{user_id}, #{email}, #{override_behaviour}, #{expected_value})" do + # The flag override uses a different config json salt than the downloaded one and + # overrides the following segments: + # * 'Beta Users': User.Email IS ONE OF ['jane@example.com'] + # * 'Developers': User.Email IS ONE OF ['john@example.com'] + options = ConfigCatOptions.new( + polling_mode: PollingMode.manual_poll, + flag_overrides: override_behaviour.nil? ? nil : LocalFileFlagOverrides.new( + File.join(script_dir, "data/test_override_segments_v6.json"), override_behaviour + ) + ) + client = ConfigCatClient.get('configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/h99HYXWWNE2bH8eWyLAVMA', options) + client.force_refresh + value = client.get_value(key, nil, User.new(user_id, email: email)) + + expect(value).to eq(expected_value) + client.close + end + end end diff --git a/spec/rollout_spec.rb b/spec/rollout_spec.rb index b922e04..8e97912 100644 --- a/spec/rollout_spec.rb +++ b/spec/rollout_spec.rb @@ -1,36 +1,108 @@ require 'spec_helper' +require_relative 'configcat/mocks' RSpec.describe 'Rollout test', type: :feature do + SCRIPT_DIR = File.dirname(__FILE__) VALUE_TEST_TYPE = "value_test" VARIATION_TEST_TYPE = "variation_test" - it "test matrix" do - test_matrix("./testmatrix.csv", "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", VALUE_TEST_TYPE) + it "test matrix basic v1" do + # https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d62463-86ec-8fde-f5b5-1c5c426fc830/244cf8b0-f604-11e8-b543-f23c917f9d8d + test_matrix("./data/testmatrix.csv", "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", VALUE_TEST_TYPE) + end + + it "test matrix semantic v1" do + # https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d745f1-f315-7daf-d163-5541d3786e6f/244cf8b0-f604-11e8-b543-f23c917f9d8d + test_matrix("./data/testmatrix_semantic.csv", "PKDVCLf-Hq-h-kCzMp-L7Q/BAr3KgLTP0ObzKnBTo5nhA", VALUE_TEST_TYPE) + end + + it "test matrix semantic 2 v1" do + # https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d77fa1-a796-85f9-df0c-57c448eb9934/244cf8b0-f604-11e8-b543-f23c917f9d8d + test_matrix("./data/testmatrix_semantic_2.csv", "PKDVCLf-Hq-h-kCzMp-L7Q/q6jMCFIp-EmuAfnmZhPY7w", VALUE_TEST_TYPE) + end + + it "test matrix number v1" do + # https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d747f0-5986-c2ef-eef3-ec778e32e10a/244cf8b0-f604-11e8-b543-f23c917f9d8d + test_matrix("./data/testmatrix_number.csv", "PKDVCLf-Hq-h-kCzMp-L7Q/uGyK3q9_ckmdxRyI7vjwCw", VALUE_TEST_TYPE) + end + + it "test matrix sensitive v1" do + # https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d7b724-9285-f4a7-9fcd-00f64f1e83d5/244cf8b0-f604-11e8-b543-f23c917f9d8d + test_matrix("./data/testmatrix_sensitive.csv", "PKDVCLf-Hq-h-kCzMp-L7Q/qX3TP2dTj06ZpCCT1h_SPA", VALUE_TEST_TYPE) + end + + it "test matrix segments old v1" do + # https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d9f207-6883-43e5-868c-cbf677af3fe6/244cf8b0-f604-11e8-b543-f23c917f9d8d + test_matrix("./data/testmatrix_segments_old.csv", "PKDVCLf-Hq-h-kCzMp-L7Q/LcYz135LE0qbcacz2mgXnA", VALUE_TEST_TYPE) + end + + it "test matrix variation id v1" do + # https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d774b9-3d05-0027-d5f4-3e76c3dba752/244cf8b0-f604-11e8-b543-f23c917f9d8d + test_matrix("./data/testmatrix_variationId.csv", "PKDVCLf-Hq-h-kCzMp-L7Q/nQ5qkhRAUEa6beEyyrVLBA", VARIATION_TEST_TYPE) + end + + it "test matrix basic" do + # https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08dbc4dc-1927-4d6b-8fb9-b1472564e2d3/244cf8b0-f604-11e8-b543-f23c917f9d8d + test_matrix("./data/testmatrix.csv", "configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/AG6C1ngVb0CvM07un6JisQ", VALUE_TEST_TYPE) end it "test matrix semantic" do - test_matrix("./testmatrix_semantic.csv", "PKDVCLf-Hq-h-kCzMp-L7Q/BAr3KgLTP0ObzKnBTo5nhA", VALUE_TEST_TYPE) + # https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08dbc4dc-278c-4f83-8d36-db73ad6e2a3a/244cf8b0-f604-11e8-b543-f23c917f9d8d + test_matrix("./data/testmatrix_semantic.csv", "configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/iV8vH2MBakKxkFZylxHmTg", VALUE_TEST_TYPE) end it "test matrix semantic 2" do - test_matrix("./testmatrix_semantic_2.csv", "PKDVCLf-Hq-h-kCzMp-L7Q/q6jMCFIp-EmuAfnmZhPY7w", VALUE_TEST_TYPE) + # https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08dbc4dc-2b2b-451e-8359-abdef494c2a2/244cf8b0-f604-11e8-b543-f23c917f9d8d + test_matrix("./data/testmatrix_semantic_2.csv", "configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/U8nt3zEhDEO5S2ulubCopA", VALUE_TEST_TYPE) end it "test matrix number" do - test_matrix("./testmatrix_number.csv", "PKDVCLf-Hq-h-kCzMp-L7Q/uGyK3q9_ckmdxRyI7vjwCw", VALUE_TEST_TYPE) + # https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08dbc4dc-0fa3-48d0-8de8-9de55b67fb8b/244cf8b0-f604-11e8-b543-f23c917f9d8d + test_matrix("./data/testmatrix_number.csv", "configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", VALUE_TEST_TYPE) end it "test matrix sensitive" do - test_matrix("./testmatrix_sensitive.csv", "PKDVCLf-Hq-h-kCzMp-L7Q/qX3TP2dTj06ZpCCT1h_SPA", VALUE_TEST_TYPE) + # https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08dbc4dc-2d62-4e1b-884b-6aa237b34764/244cf8b0-f604-11e8-b543-f23c917f9d8d + test_matrix("./data/testmatrix_sensitive.csv", "configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/-0YmVOUNgEGKkgRF-rU65g", VALUE_TEST_TYPE) + end + + it "test matrix segments old" do + # https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08dbd6ca-a85f-4ed0-888a-2da18def92b5/244cf8b0-f604-11e8-b543-f23c917f9d8d + test_matrix("./data/testmatrix_segments_old.csv", "configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/y_ZB7o-Xb0Swxth-ZlMSeA", VALUE_TEST_TYPE) end it "test matrix variation id" do - test_matrix("./testmatrix_variationId.csv", "PKDVCLf-Hq-h-kCzMp-L7Q/nQ5qkhRAUEa6beEyyrVLBA", VARIATION_TEST_TYPE) + # https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08dbc4dc-30c6-4969-8e4c-03f6a8764199/244cf8b0-f604-11e8-b543-f23c917f9d8d + test_matrix("./data/testmatrix_variationId.csv", "configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/spQnkRTIPEWVivZkWM84lQ", VARIATION_TEST_TYPE) + end + + it "test matrix comparators v6" do + # https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9a6b-4947-84e2-91529248278a/08dbc325-9ebd-4587-8171-88f76a3004cb + test_matrix('./data/testmatrix_comparators_v6.csv', 'configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ', VALUE_TEST_TYPE) + end + + it "test matrix segments" do + # https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9cfb-486f-8906-72a57c693615/08dbc325-9ebd-4587-8171-88f76a3004cb + test_matrix('data/testmatrix_segments.csv', 'configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/h99HYXWWNE2bH8eWyLAVMA', VALUE_TEST_TYPE) + end + + it "test matrix prerequisite flag" do + # https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9b74-45cb-86d0-4d61c25af1aa/08dbc325-9ebd-4587-8171-88f76a3004cb + test_matrix('data/testmatrix_prerequisite_flag.csv', 'configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/JoGwdqJZQ0K2xDy7LnbyOg', VALUE_TEST_TYPE) + end + + it "test matrix and or" do + # https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9d5e-4988-891c-fd4a45790bd1/08dbc325-9ebd-4587-8171-88f76a3004cb + test_matrix('data/testmatrix_and_or.csv', 'configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/ByMO9yZNn02kXcm72lnY1A', VALUE_TEST_TYPE) + end + + it "test_matrix_unicode" do + # https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbd63c-9774-49d6-8187-5f2aab7bd606/08dbc325-9ebd-4587-8171-88f76a3004cb + test_matrix('data/testmatrix_unicode.csv', 'configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/Da6w8dBbmUeMUBhh0iEeQQ', VALUE_TEST_TYPE) end def test_matrix(file_path, sdk_key, type) - script_dir = File.dirname(__FILE__) - file_path = File.join(script_dir, file_path) + file_path = File.join(SCRIPT_DIR, file_path) content = "" open(file_path, "r") { |f| content = f.readlines() @@ -79,4 +151,398 @@ def test_matrix(file_path, sdk_key, type) expect(setting_value).to eq "Cat" ConfigCat.close_all() end + + [ + ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", "stringMatchedTargetingRuleAndOrPercentageOption", nil, nil, nil, "Cat", false, false], + ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", "stringMatchedTargetingRuleAndOrPercentageOption", "12345", nil, nil, "Cat", false, false], + ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", "stringMatchedTargetingRuleAndOrPercentageOption", "12345", "a@example.com", nil, "Dog", true, false], + ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", "stringMatchedTargetingRuleAndOrPercentageOption", "12345", "a@configcat.com", nil, "Cat", false, false], + ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", "stringMatchedTargetingRuleAndOrPercentageOption", "12345", "a@configcat.com", "", "Frog", true, true], + ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", "stringMatchedTargetingRuleAndOrPercentageOption", "12345", "a@configcat.com", "US", "Fish", true, true], + ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", "stringMatchedTargetingRuleAndOrPercentageOption", "12345", "b@configcat.com", nil, "Cat", false, false], + ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", "stringMatchedTargetingRuleAndOrPercentageOption", "12345", "b@configcat.com", "", "Falcon", false, true], + ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", "stringMatchedTargetingRuleAndOrPercentageOption", "12345", "b@configcat.com", "US", "Spider", false, true] + ].each do |sdk_key, key, user_id, email, percentage_base, expected_return_value, expected_matched_targeting_rule, expected_matched_percentage_option| + it "test_evaluation_details_matched_evaluation_rule_and_percentage_option (#{sdk_key}, #{key}, #{user_id}, #{email}, #{percentage_base}, #{expected_return_value}, #{expected_matched_targeting_rule}, #{expected_matched_percentage_option})" do + client = ConfigCat.get(sdk_key, ConfigCat::ConfigCatOptions.new(polling_mode: ConfigCat::PollingMode.manual_poll)) + client.force_refresh + + user = user_id ? ConfigCat::User.new(user_id, email: email, custom: { "PercentageBase" => percentage_base }) : nil + + evaluation_details = client.get_value_details(key, nil, user) + + expect(evaluation_details.value).to eq(expected_return_value) + expect(!!evaluation_details.matched_targeting_rule).to eq(expected_matched_targeting_rule) + expect(!!evaluation_details.matched_percentage_option).to eq(expected_matched_percentage_option) + end + end + + it "test user object attribute value conversion text comparison" do + begin + # Setup logging + logger = ConfigCat.logger + log_stream = StringIO.new + ConfigCat.logger = Logger.new(log_stream, level: Logger::WARN) + + client = ConfigCatClient.get("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", ConfigCatOptions.new(polling_mode: PollingMode.manual_poll)) + client.force_refresh + + custom_attribute_name = 'Custom1' + custom_attribute_value = 42 + user = User.new('12345', custom: { custom_attribute_name => custom_attribute_value }) + + key = 'boolTextEqualsNumber' + value = client.get_value(key, nil, user) + expect(value).to eq(true) + + log_stream.rewind + log = log_stream.read + + expect(log).to include("[3005] Evaluation of condition (User.#{custom_attribute_name} EQUALS '#{custom_attribute_value}') " \ + "for setting '#{key}' may not produce the expected result (the User.#{custom_attribute_name} " \ + "attribute is not a string value, thus it was automatically converted to the string value " \ + "'#{custom_attribute_value}'). Please make sure that using a non-string value was intended.") + ensure + client.close + ConfigCat.logger = logger + end + end + + it "test wrong config json type mismatch" do + begin + config = { + 'f' => { + 'test' => { + 't' => 1, # SettingType.STRING + 'v' => { 'b' => true }, # bool value instead of string (type mismatch) + 'p' => [], + 'r' => [] + } + } + } + + # Setup logging + logger = ConfigCat.logger + log_stream = StringIO.new + ConfigCat.logger = Logger.new(log_stream, level: Logger::ERROR) + log = ConfigCatLogger.new(Hooks.new) + evaluator = RolloutEvaluator.new(log) + + value, = evaluator.evaluate(key: 'test', user: nil, default_value: false, default_variation_id: 'default_variation_id', + config: config, log_builder: nil) + + expect(value).to be false + log_stream.rewind + error_log = log_stream.read + expect(error_log).to include("[2001] Failed to evaluate setting 'test'. " \ + "(Setting value is not of the expected type String)") + ensure + ConfigCat.logger = logger + end + end + + [ + # SemVer-based comparisons + ["configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/iV8vH2MBakKxkFZylxHmTg", "lessThanWithPercentage", "12345", "Custom1", "0.0", "20%"], + ["configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/iV8vH2MBakKxkFZylxHmTg", "lessThanWithPercentage", "12345", "Custom1", "0.9.9", "< 1.0.0"], + ["configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/iV8vH2MBakKxkFZylxHmTg", "lessThanWithPercentage", "12345", "Custom1", "1.0.0", "20%"], + ["configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/iV8vH2MBakKxkFZylxHmTg", "lessThanWithPercentage", "12345", "Custom1", "1.1", "20%"], + ["configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/iV8vH2MBakKxkFZylxHmTg", "lessThanWithPercentage", "12345", "Custom1", 0, "20%"], + ["configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/iV8vH2MBakKxkFZylxHmTg", "lessThanWithPercentage", "12345", "Custom1", 0.9, "20%"], + ["configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/iV8vH2MBakKxkFZylxHmTg", "lessThanWithPercentage", "12345", "Custom1", 2, "20%"], + # Number-based comparisons + ["configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", -Float::INFINITY, "<2.1"], + ["configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", -1, "<2.1"], + ["configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", 2, "<2.1"], + ["configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", 2.1, "<=2,1"], + ["configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", 3, "<>4.2"], + ["configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", 5, ">=5"], + ["configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", Float::INFINITY, ">5"], + ["configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", Float::NAN, "<>4.2"], + ["configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "-Infinity", "<2.1"], + ["configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "-1", "<2.1"], + ["configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "2", "<2.1"], + ["configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "2.1", "<=2,1"], + ["configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "2,1", "<=2,1"], + ["configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "3", "<>4.2"], + ["configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "5", ">=5"], + ["configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "Infinity", ">5"], + ["configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "NaN", "<>4.2"], + ["configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "NaNa", "80%"], + # Date time-based comparisons + ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", DateTime.parse("2023-03-31T23:59:59.9990000Z"), false], + ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", DateTime.parse("2023-04-01T01:59:59.9990000+02:00"), false], + ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", DateTime.parse("2023-04-01T00:00:00.0010000Z"), true], + ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", DateTime.parse("2023-04-01T02:00:00.0010000+02:00"), true], + ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", DateTime.parse("2023-04-30T23:59:59.9990000Z"), true], + ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", DateTime.parse("2023-05-01T01:59:59.9990000+02:00"), true], + ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", DateTime.parse("2023-05-01T00:00:00.0010000Z"), false], + ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", DateTime.parse("2023-05-01T02:00:00.0010000+02:00"), false], + ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", -Float::INFINITY, false], + ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", 1680307199.999, false], + ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", 1680307200.001, true], + ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", 1682899199.999, true], + ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", 1682899200.001, false], + ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", Float::INFINITY, false], + ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", Float::NAN, false], + ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", 1680307199, false], + ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", 1680307201, true], + ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", 1682899199, true], + ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", 1682899201, false], + ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "-Infinity", false], + ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "1680307199.999", false], + ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "1680307200.001", true], + ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "1682899199.999", true], + ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "1682899200.001", false], + ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "+Infinity", false], + ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "NaN", false], + # String array-based comparisons + ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "stringArrayContainsAnyOfDogDefaultCat", "12345", "Custom1", ["x", "read"], "Dog"], + ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "stringArrayContainsAnyOfDogDefaultCat", "12345", "Custom1", ["x", "Read"], "Cat"], + ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "stringArrayContainsAnyOfDogDefaultCat", "12345", "Custom1", "[\"x\", \"read\"]", "Dog"], + ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "stringArrayContainsAnyOfDogDefaultCat", "12345", "Custom1", "[\"x\", \"Read\"]", "Cat"], + ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "stringArrayContainsAnyOfDogDefaultCat", "12345", "Custom1", "x, read", "Cat"] + ].each do |sdk_key, key, user_id, custom_attribute_name, custom_attribute_value, expected_return_value| + it "test_user_object_attribute_value_conversion_non_text_comparisons (#{sdk_key}, #{key}, #{user_id}, #{custom_attribute_name}, #{custom_attribute_value}, #{custom_attribute_value.class}, #{expected_return_value})" do + client = ConfigCat.get(sdk_key, ConfigCat::ConfigCatOptions.new(polling_mode: ConfigCat::PollingMode.manual_poll)) + client.force_refresh + user = ConfigCat::User.new(user_id, custom: { custom_attribute_name => custom_attribute_value }) + value = client.get_value(key, nil, user) + + expect(value).to eq(expected_return_value) + client.close + end + end + + [ + ["numberToStringConversion", 0.12345, "1"], + ["numberToStringConversionInt", 125.0, "4"], + ["numberToStringConversionPositiveExp", -1.23456789e96, "2"], + ["numberToStringConversionNegativeExp", -12345.6789E-100, "4"], + ["numberToStringConversionNaN", Float::NAN, "3"], + ["numberToStringConversionPositiveInf", Float::INFINITY, "4"], + ["numberToStringConversionNegativeInf", -Float::INFINITY, "3"], + ["dateToStringConversion", DateTime.parse("2023-03-31T23:59:59.9990000Z"), "3"], + ["dateToStringConversion", 1680307199.999, "3"], # Assuming this needs conversion to date + ["dateToStringConversionNaN", Float::NAN, "3"], + ["dateToStringConversionPositiveInf", Float::INFINITY, "1"], + ["dateToStringConversionNegativeInf", -Float::INFINITY, "5"], + ["stringArrayToStringConversion", ["read", "Write", " eXecute "], "4"], + ["stringArrayToStringConversionEmpty", [], "5"], + ["stringArrayToStringConversionSpecialChars", ["+<>%\"'\\/\t\r\n"], "3"], + ["stringArrayToStringConversionUnicode", ["äöüÄÖÜçéèñışğ⢙✓😀"], "2"], + ].each do |key, custom_attribute_value, expected_return_value| + it "test_attribute_conversion_to_canonical_string (#{key}, #{custom_attribute_value}, #{expected_return_value})" do + config = LocalFileDataSource.new(File.join(SCRIPT_DIR, "data/comparison_attribute_conversion.json"), OverrideBehaviour::LOCAL_ONLY, nil).get_overrides + + log = ConfigCatLogger.new(Hooks.new) + evaluator = RolloutEvaluator.new(log) + user = ConfigCat::User.new("12345", custom: { "Custom1" => custom_attribute_value }) + + value, = evaluator.evaluate(key: key, user: user, default_value: 'default_value', default_variation_id: 'default_variation_id', + config: config, log_builder: nil) + expect(value).to eq(expected_return_value) + end + end + + [ + ["isoneof", "no trim"], + ["isnotoneof", "no trim"], + ["isoneofhashed", "no trim"], + ["isnotoneofhashed", "no trim"], + ["equalshashed", "no trim"], + ["notequalshashed", "no trim"], + ["arraycontainsanyofhashed", "no trim"], + ["arraynotcontainsanyofhashed", "no trim"], + ["equals", "no trim"], + ["notequals", "no trim"], + ["startwithanyof", "no trim"], + ["notstartwithanyof", "no trim"], + ["endswithanyof", "no trim"], + ["notendswithanyof", "no trim"], + ["arraycontainsanyof", "no trim"], + ["arraynotcontainsanyof", "no trim"], + ["startwithanyofhashed", "no trim"], + ["notstartwithanyofhashed", "no trim"], + ["endswithanyofhashed", "no trim"], + ["notendswithanyofhashed", "no trim"], + # semver comparators user values trimmed because of backward compatibility + ["semverisoneof", "4 trim"], + ["semverisnotoneof", "5 trim"], + ["semverless", "6 trim"], + ["semverlessequals", "7 trim"], + ["semvergreater", "8 trim"], + ["semvergreaterequals", "9 trim"], + # number and date comparators user values trimmed because of backward compatibility + ["numberequals", "10 trim"], + ["numbernotequals", "11 trim"], + ["numberless", "12 trim"], + ["numberlessequals", "13 trim"], + ["numbergreater", "14 trim"], + ["numbergreaterequals", "15 trim"], + ["datebefore", "18 trim"], + ["dateafter", "19 trim"], + # "contains any of" and "not contains any of" is a special case, the not trimmed user attribute checked against not trimmed comparator values. + ["containsanyof", "no trim"], + ["notcontainsanyof", "no trim"], + ].each do |key, expected_return_value| + it "test_comparison_attribute_trimming (#{key}, #{expected_return_value})" do + config = LocalFileDataSource.new(File.join(SCRIPT_DIR, "data/comparison_attribute_trimming.json"), OverrideBehaviour::LOCAL_ONLY, nil).get_overrides + + log = ConfigCatLogger.new(Hooks.new) + evaluator = RolloutEvaluator.new(log) + user = ConfigCat::User.new(" 12345 ", country: '[" USA "]', custom: { + 'Version' => ' 1.0.0 ', + 'Number' => ' 3 ', + 'Date' => ' 1705253400 ' + }) + value, = evaluator.evaluate(key: key, user: user, default_value: 'default_value', default_variation_id: 'default_variation_id', + config: config, log_builder: nil) + expect(value).to eq(expected_return_value) + end + end + + [ + ["isoneof", "no trim"], + ["isnotoneof", "no trim"], + ["isoneofhashed", "no trim"], + ["isnotoneofhashed", "no trim"], + ["equalshashed", "no trim"], + ["notequalshashed", "no trim"], + ["arraycontainsanyofhashed", "no trim"], + ["arraynotcontainsanyofhashed", "no trim"], + ["equals", "no trim"], + ["notequals", "no trim"], + ["startwithanyof", "no trim"], + ["notstartwithanyof", "no trim"], + ["endswithanyof", "no trim"], + ["notendswithanyof", "no trim"], + ["arraycontainsanyof", "no trim"], + ["arraynotcontainsanyof", "no trim"], + ["startwithanyofhashed", "no trim"], + ["notstartwithanyofhashed", "no trim"], + ["endswithanyofhashed", "no trim"], + ["notendswithanyofhashed", "no trim"], + # semver comparators user values trimmed because of backward compatibility + ["semverisoneof", "4 trim"], + ["semverisnotoneof", "5 trim"], + ["semverless", "6 trim"], + ["semverlessequals", "7 trim"], + ["semvergreater", "8 trim"], + ["semvergreaterequals", "9 trim"] + ].each do |key, expected_return_value| + it "test_comparison_value_trimming (#{key}, #{expected_return_value})" do + config = LocalFileDataSource.new(File.join(SCRIPT_DIR, "data/comparison_value_trimming.json"), OverrideBehaviour::LOCAL_ONLY, nil).get_overrides + + log = ConfigCatLogger.new(Hooks.new) + evaluator = RolloutEvaluator.new(log) + user = ConfigCat::User.new("12345", country: '["USA"]', custom: { + 'Version' => '1.0.0', + 'Number' => '3', + 'Date' => '1705253400' + }) + value, = evaluator.evaluate(key: key, user: user, default_value: 'default_value', default_variation_id: 'default_variation_id', + config: config, log_builder: nil) + expect(value).to eq(expected_return_value) + end + end + + [ + ["key1", "'key1' -> 'key1'"], + ["key2", "'key2' -> 'key3' -> 'key2'"], + ["key4", "'key4' -> 'key3' -> 'key2' -> 'key3'"] + ].each do |key, dependency_cycle| + it "test_prerequisite_flag_circular_dependency (#{key}, #{dependency_cycle})" do + begin + config = LocalFileDataSource.new(File.join(SCRIPT_DIR, "data/test_circulardependency_v6.json"), OverrideBehaviour::LOCAL_ONLY, nil).get_overrides + + # Setup logging + logger = ConfigCat.logger + log_stream = StringIO.new + ConfigCat.logger = Logger.new(log_stream, level: Logger::ERROR) + + log = ConfigCatLogger.new(Hooks.new) + evaluator = RolloutEvaluator.new(log) + value, = evaluator.evaluate(key: key, user: nil, default_value: 'default_value', default_variation_id: 'default_variation_id', + config: config, log_builder: nil) + expect(value).to eq('default_value') + log_stream.rewind + error_log = log_stream.read + expect(error_log).to include("Circular dependency detected") + expect(error_log).to include(dependency_cycle) + ensure + ConfigCat.logger = logger + end + end + end + + [ + ["stringDependsOnBool", "mainBoolFlag", true, "Dog"], + ["stringDependsOnBool", "mainBoolFlag", false, "Cat"], + ["stringDependsOnBool", "mainBoolFlag", "1", nil], + ["stringDependsOnBool", "mainBoolFlag", 1, nil], + ["stringDependsOnBool", "mainBoolFlag", 1.0, nil], + ["stringDependsOnBool", "mainBoolFlag", [true], nil], + ["stringDependsOnBool", "mainBoolFlag", nil, nil], + ["stringDependsOnString", "mainStringFlag", "private", "Dog"], + ["stringDependsOnString", "mainStringFlag", "Private", "Cat"], + ["stringDependsOnString", "mainStringFlag", true, nil], + ["stringDependsOnString", "mainStringFlag", 1, nil], + ["stringDependsOnString", "mainStringFlag", 1.0, nil], + ["stringDependsOnString", "mainStringFlag", ["private"], nil], + ["stringDependsOnString", "mainStringFlag", nil, nil], + ["stringDependsOnInt", "mainIntFlag", 2, "Dog"], + ["stringDependsOnInt", "mainIntFlag", 1, "Cat"], + ["stringDependsOnInt", "mainIntFlag", "2", nil], + ["stringDependsOnInt", "mainIntFlag", true, nil], + ["stringDependsOnInt", "mainIntFlag", 2.0, nil], + ["stringDependsOnInt", "mainIntFlag", [2], nil], + ["stringDependsOnInt", "mainIntFlag", nil, nil], + ["stringDependsOnDouble", "mainDoubleFlag", 0.1, "Dog"], + ["stringDependsOnDouble", "mainDoubleFlag", 0.11, "Cat"], + ["stringDependsOnDouble", "mainDoubleFlag", "0.1", nil], + ["stringDependsOnDouble", "mainDoubleFlag", true, nil], + ["stringDependsOnDouble", "mainDoubleFlag", 1, nil], + ["stringDependsOnDouble", "mainDoubleFlag", [0.1], nil], + ["stringDependsOnDouble", "mainDoubleFlag", nil, nil], + ].each do |key, prerequisite_flag_key, prerequisite_flag_value, expected_value| + it "test_prerequisite_flag_comparison_value_type_mismatch (#{key}, #{prerequisite_flag_key}, #{prerequisite_flag_value}, #{expected_value})" do + begin + # Setup logging + logger = ConfigCat.logger + log_stream = StringIO.new + ConfigCat.logger = Logger.new(log_stream, level: Logger::WARN) + + override_dictionary = { prerequisite_flag_key => prerequisite_flag_value } + options = ConfigCatOptions.new( + polling_mode: PollingMode.manual_poll, + flag_overrides: ConfigCat::LocalDictionaryFlagOverrides.new( + override_dictionary, + ConfigCat::OverrideBehaviour::LOCAL_OVER_REMOTE + ) + ) + client = ConfigCatClient.get("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/JoGwdqJZQ0K2xDy7LnbyOg", options) + client.force_refresh + + value = client.get_value(key, nil) + expect(value).to eq(expected_value) + + if expected_value.nil? + log_stream.rewind + error_log = log_stream.read + prerequisite_flag_value_type = SettingType.to_type(SettingType.from_type(prerequisite_flag_value.class)) + + if prerequisite_flag_value.nil? || prerequisite_flag_value_type.nil? + expect(error_log).to include('Unsupported setting type') + else + expect(error_log).to include("Setting value is not of the expected type #{prerequisite_flag_value_type}") + end + end + ensure + client.close + ConfigCat.logger = logger + end + end + end + end diff --git a/spec/specialcharacter_spec.rb b/spec/specialcharacter_spec.rb new file mode 100644 index 0000000..111eaa4 --- /dev/null +++ b/spec/specialcharacter_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +RSpec.describe 'Special character test', type: :feature do + before(:all) do + @client = ConfigCat.get('configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/u28_1qNyZ0Wz-ldYHIU7-g') + end + + after(:all) do + @client.close + end + + it "test_special_characters_works_cleartext" do + actual = @client.get_value("specialCharacters", "NOT_CAT", ConfigCat::User.new('äöüÄÖÜçéèñışğ⢙✓😀')) + expect(actual).to eq('äöüÄÖÜçéèñışğ⢙✓😀') + end + + it "test_special_characters_works_hashed" do + actual = @client.get_value("specialCharactersHashed", "NOT_CAT", ConfigCat::User.new('äöüÄÖÜçéèñışğ⢙✓😀')) + expect(actual).to eq('äöüÄÖÜçéèñışğ⢙✓😀') + end + +end diff --git a/spec/test.json b/spec/test.json deleted file mode 100644 index d547507..0000000 --- a/spec/test.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "f": { - "disabledFeature": { - "v": false - }, - "enabledFeature": { - "v": true - }, - "intSetting": { - "v": 5 - }, - "doubleSetting": { - "v": 3.14 - }, - "stringSetting": { - "v": "test" - } - } -} \ No newline at end of file