From 60f4f5e14a1db3898834a22e44b30d850b7dc3c7 Mon Sep 17 00:00:00 2001 From: adams85 <31276480+adams85@users.noreply.github.com> Date: Mon, 4 Mar 2024 13:43:46 +0100 Subject: [PATCH] Config v6 (#44) * Add VSCode devenv configuration * Run tests on PHP 8.3 * Update config JSON model to v6 + fix inconsistent error 1000 reporting + improve config json deserialization error reporting * Refactor evaluator and evaluation logging to prepare it for the new features * Implement new comparison operators * Implement segment condition evaluation * Implement prerequisite flag condition evaluation * Implement SDK key format validation + fix broken tests * Rename EvaluationDetails.getMatched* properties * Improve message of error 1103 * Add matrix tests * Align config json error handling of EvaluateLogBuilder with error reporting of RolloutEvaluator * Add exception to the log format in DefaultLogger * Add tests for evaluation logging + minor fixes * Add User Object tests * Add tests for sdk key format validation * Add user attribute conversion tests + re-implement Utils.numberToString as NumberFormatter requires a PHP extension * Add comparison attribute and comparison value trimming tests * Add special character tests * Add tests for EvaluationDetails.getMatchedTargetingRule/getMatchedPercentageOption properties * Add flag overrides vs. prerequisite flags, config json salt and segments tests + add related exception object to EvaluationDetails * Add override value type mismatch tests * Bump version * Update package version in samples --------- Co-authored-by: Peter Adam Korodi <52385411+kp-cat@users.noreply.github.com> --- .github/workflows/php-ci.yml | 2 +- .php-cs-fixer.php | 7 +- .vscode/extensions.json | 21 + .vscode/launch.json | 23 + .vscode/settings.json | 16 + phpdebug.bat | 4 + samples/consolesample/composer.json | 2 +- samples/laravel/composer.json | 2 +- src/Attributes/Config.php | 14 - src/Attributes/PercentageAttributes.php | 15 - src/Attributes/Preferences.php | 14 - src/Attributes/RolloutAttributes.php | 17 - src/Attributes/SettingAttributes.php | 17 - src/Cache/ConfigCache.php | 6 +- src/Cache/ConfigEntry.php | 6 +- src/ConfigCatClient.php | 157 +- src/ConfigFetcher.php | 38 +- src/ConfigJson/ConditionContainer.php | 62 + src/ConfigJson/Config.php | 68 + src/ConfigJson/PercentageOption.php | 47 + src/ConfigJson/Preferences.php | 26 + src/ConfigJson/PrerequisiteFlagComparator.php | 17 + src/ConfigJson/PrerequisiteFlagCondition.php | 15 + src/ConfigJson/RedirectMode.php | 15 + src/ConfigJson/Segment.php | 60 + src/ConfigJson/SegmentComparator.php | 17 + src/ConfigJson/SegmentCondition.php | 14 + src/ConfigJson/Setting.php | 124 ++ src/ConfigJson/SettingType.php | 23 + src/ConfigJson/SettingValue.php | 97 + src/ConfigJson/SettingValueContainer.php | 14 + src/ConfigJson/TargetingRule.php | 76 + src/ConfigJson/UserComparator.php | 119 ++ src/ConfigJson/UserCondition.php | 17 + src/EvaluateLogBuilder.php | 431 +++++ src/EvaluationDetails.php | 44 +- src/EvaluationLogCollector.php | 26 - src/Log/DefaultLogger.php | 8 +- src/Log/InternalLogger.php | 4 +- src/Override/ArrayDataSource.php | 6 +- src/Override/LocalFileDataSource.php | 50 +- src/RolloutEvaluator.php | 1609 +++++++++++++---- src/SettingsResult.php | 4 +- src/User.php | 120 +- src/Utils.php | 198 +- tests/CacheTest.php | 8 +- tests/ConfigCatClientTest.php | 126 +- tests/ConfigFetcherTest.php | 1 + tests/ConfigV2EvaluationTests.php | 616 +++++++ tests/DataGovernanceTest.php | 24 +- tests/EvaluationLogTest.php | 137 ++ tests/Helpers/FakeLogger.php | 98 + tests/Helpers/Utils.php | 100 + tests/LocalSourceTest.php | 12 +- tests/LoggerTest.php | 2 +- tests/OverrideTest.php | 188 ++ tests/RolloutIntegrationsTest.php | 81 +- tests/UserTest.php | 142 ++ tests/Utils.php | 42 - tests/UtilsTest.php | 57 + .../data/comparison_attribute_conversion.json | 789 ++++++++ tests/data/comparison_attribute_trimming.json | 985 ++++++++++ tests/data/comparison_value_trimming.json | 777 ++++++++ .../data/evaluationlog/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 + .../data/evaluationlog/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 + .../_overrides/test_list_truncation.json | 83 + tests/data/evaluationlog/and_rules.json | 22 + .../and_rules/and_rules_no_user.txt | 7 + .../and_rules/and_rules_user.txt | 7 + tests/data/evaluationlog/comparators.json | 20 + .../evaluationlog/comparators/allinone.txt | 57 + .../evaluationlog/epoch_date_validation.json | 16 + .../epoch_date_validation/date_error.txt | 7 + tests/data/evaluationlog/list_truncation.json | 14 + .../list_truncation/list_truncation.txt | 7 + .../data/evaluationlog/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 + .../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 + .../data/evaluationlog/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 + tests/data/evaluationlog/segment.json | 47 + .../segment/segment_matching.txt | 11 + .../segment/segment_no_matching.txt | 11 + .../segment/segment_no_targeted_attribute.txt | 13 + .../evaluationlog/segment/segment_no_user.txt | 6 + .../segment_no_user_multi_conditions.txt | 7 + .../data/evaluationlog/semver_validation.json | 26 + .../semver_validation/semver_error.txt | 9 + .../semver_relations_error.txt | 18 + tests/data/evaluationlog/simple_value.json | 37 + .../simple_value/double_setting.txt | 2 + .../simple_value/int_setting.txt | 2 + .../evaluationlog/simple_value/off_flag.txt | 2 + .../evaluationlog/simple_value/on_flag.txt | 2 + .../simple_value/text_setting.txt | 2 + tests/data/test-rules.json | 30 + tests/{ => data}/test-simple.json | 0 tests/data/test.json | 34 + tests/data/test_circulardependency_v6.json | 80 + .../data/test_override_flagdependency_v6.json | 44 + tests/data/test_override_segments_v6.json | 66 + tests/{ => data}/testmatrix.csv | 462 ++--- tests/data/testmatrix_and_or.csv | 15 + tests/data/testmatrix_comparators_v6.csv | 24 + tests/{ => data}/testmatrix_number.csv | 2 +- tests/data/testmatrix_prerequisite_flag.csv | 5 + tests/data/testmatrix_segments.csv | 6 + tests/data/testmatrix_segments_old.csv | 6 + tests/{ => data}/testmatrix_semantic.csv | 2 +- tests/{ => data}/testmatrix_semantic_2.csv | 0 tests/{ => data}/testmatrix_sensitive.csv | 0 tests/data/testmatrix_unicode.csv | 14 + tests/{ => data}/testmatrix_variationId.csv | 14 +- tests/test-rules.json | 16 - tests/test.json | 19 - 144 files changed, 8608 insertions(+), 1070 deletions(-) create mode 100644 .vscode/extensions.json create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 phpdebug.bat delete mode 100644 src/Attributes/Config.php delete mode 100644 src/Attributes/PercentageAttributes.php delete mode 100644 src/Attributes/Preferences.php delete mode 100644 src/Attributes/RolloutAttributes.php delete mode 100644 src/Attributes/SettingAttributes.php create mode 100644 src/ConfigJson/ConditionContainer.php create mode 100644 src/ConfigJson/Config.php create mode 100644 src/ConfigJson/PercentageOption.php create mode 100644 src/ConfigJson/Preferences.php create mode 100644 src/ConfigJson/PrerequisiteFlagComparator.php create mode 100644 src/ConfigJson/PrerequisiteFlagCondition.php create mode 100644 src/ConfigJson/RedirectMode.php create mode 100644 src/ConfigJson/Segment.php create mode 100644 src/ConfigJson/SegmentComparator.php create mode 100644 src/ConfigJson/SegmentCondition.php create mode 100644 src/ConfigJson/Setting.php create mode 100644 src/ConfigJson/SettingType.php create mode 100644 src/ConfigJson/SettingValue.php create mode 100644 src/ConfigJson/SettingValueContainer.php create mode 100644 src/ConfigJson/TargetingRule.php create mode 100644 src/ConfigJson/UserComparator.php create mode 100644 src/ConfigJson/UserCondition.php create mode 100644 src/EvaluateLogBuilder.php delete mode 100644 src/EvaluationLogCollector.php create mode 100644 tests/ConfigV2EvaluationTests.php create mode 100644 tests/EvaluationLogTest.php create mode 100644 tests/Helpers/FakeLogger.php create mode 100644 tests/Helpers/Utils.php create mode 100644 tests/OverrideTest.php delete mode 100644 tests/Utils.php create mode 100644 tests/UtilsTest.php create mode 100644 tests/data/comparison_attribute_conversion.json create mode 100644 tests/data/comparison_attribute_trimming.json create mode 100644 tests/data/comparison_value_trimming.json create mode 100644 tests/data/evaluationlog/1_targeting_rule.json create mode 100644 tests/data/evaluationlog/1_targeting_rule/1_rule_matching_targeted_attribute.txt create mode 100644 tests/data/evaluationlog/1_targeting_rule/1_rule_no_targeted_attribute.txt create mode 100644 tests/data/evaluationlog/1_targeting_rule/1_rule_no_user.txt create mode 100644 tests/data/evaluationlog/1_targeting_rule/1_rule_not_matching_targeted_attribute.txt create mode 100644 tests/data/evaluationlog/2_targeting_rules.json create mode 100644 tests/data/evaluationlog/2_targeting_rules/2_rules_matching_targeted_attribute.txt create mode 100644 tests/data/evaluationlog/2_targeting_rules/2_rules_no_targeted_attribute.txt create mode 100644 tests/data/evaluationlog/2_targeting_rules/2_rules_no_user.txt create mode 100644 tests/data/evaluationlog/2_targeting_rules/2_rules_not_matching_targeted_attribute.txt create mode 100644 tests/data/evaluationlog/_overrides/test_list_truncation.json create mode 100644 tests/data/evaluationlog/and_rules.json create mode 100644 tests/data/evaluationlog/and_rules/and_rules_no_user.txt create mode 100644 tests/data/evaluationlog/and_rules/and_rules_user.txt create mode 100644 tests/data/evaluationlog/comparators.json create mode 100644 tests/data/evaluationlog/comparators/allinone.txt create mode 100644 tests/data/evaluationlog/epoch_date_validation.json create mode 100644 tests/data/evaluationlog/epoch_date_validation/date_error.txt create mode 100644 tests/data/evaluationlog/list_truncation.json create mode 100644 tests/data/evaluationlog/list_truncation/list_truncation.txt create mode 100644 tests/data/evaluationlog/number_validation.json create mode 100644 tests/data/evaluationlog/number_validation/number_error.txt create mode 100644 tests/data/evaluationlog/options_after_targeting_rule.json create mode 100644 tests/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_matching_targeted_attribute.txt create mode 100644 tests/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_no_targeted_attribute.txt create mode 100644 tests/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_no_user.txt create mode 100644 tests/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_not_matching_targeted_attribute.txt create mode 100644 tests/data/evaluationlog/options_based_on_custom_attr.json create mode 100644 tests/data/evaluationlog/options_based_on_custom_attr/matching_options_custom_attribute.txt create mode 100644 tests/data/evaluationlog/options_based_on_custom_attr/no_options_custom_attribute.txt create mode 100644 tests/data/evaluationlog/options_based_on_custom_attr/options_custom_attribute_no_user.txt create mode 100644 tests/data/evaluationlog/options_based_on_user_id.json create mode 100644 tests/data/evaluationlog/options_based_on_user_id/options_user_attribute_no_user.txt create mode 100644 tests/data/evaluationlog/options_based_on_user_id/options_user_attribute_user.txt create mode 100644 tests/data/evaluationlog/options_within_targeting_rule.json create mode 100644 tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_no_options_attribute.txt create mode 100644 tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_options_attribute.txt create mode 100644 tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_no_targeted_attribute.txt create mode 100644 tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_no_user.txt create mode 100644 tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_not_matching_targeted_attribute.txt create mode 100644 tests/data/evaluationlog/prerequisite_flag.json create mode 100644 tests/data/evaluationlog/prerequisite_flag/prerequisite_flag.txt create mode 100644 tests/data/evaluationlog/prerequisite_flag/prerequisite_flag_multilevel.txt create mode 100644 tests/data/evaluationlog/prerequisite_flag/prerequisite_flag_no_user_needed_by_both.txt create mode 100644 tests/data/evaluationlog/prerequisite_flag/prerequisite_flag_no_user_needed_by_dep.txt create mode 100644 tests/data/evaluationlog/prerequisite_flag/prerequisite_flag_no_user_needed_by_prereq.txt create mode 100644 tests/data/evaluationlog/segment.json create mode 100644 tests/data/evaluationlog/segment/segment_matching.txt create mode 100644 tests/data/evaluationlog/segment/segment_no_matching.txt create mode 100644 tests/data/evaluationlog/segment/segment_no_targeted_attribute.txt create mode 100644 tests/data/evaluationlog/segment/segment_no_user.txt create mode 100644 tests/data/evaluationlog/segment/segment_no_user_multi_conditions.txt create mode 100644 tests/data/evaluationlog/semver_validation.json create mode 100644 tests/data/evaluationlog/semver_validation/semver_error.txt create mode 100644 tests/data/evaluationlog/semver_validation/semver_relations_error.txt create mode 100644 tests/data/evaluationlog/simple_value.json create mode 100644 tests/data/evaluationlog/simple_value/double_setting.txt create mode 100644 tests/data/evaluationlog/simple_value/int_setting.txt create mode 100644 tests/data/evaluationlog/simple_value/off_flag.txt create mode 100644 tests/data/evaluationlog/simple_value/on_flag.txt create mode 100644 tests/data/evaluationlog/simple_value/text_setting.txt create mode 100644 tests/data/test-rules.json rename tests/{ => data}/test-simple.json (100%) create mode 100644 tests/data/test.json create mode 100644 tests/data/test_circulardependency_v6.json create mode 100644 tests/data/test_override_flagdependency_v6.json create mode 100644 tests/data/test_override_segments_v6.json rename tests/{ => data}/testmatrix.csv (84%) create mode 100644 tests/data/testmatrix_and_or.csv create mode 100644 tests/data/testmatrix_comparators_v6.csv rename tests/{ => data}/testmatrix_number.csv (96%) create mode 100644 tests/data/testmatrix_prerequisite_flag.csv create mode 100644 tests/data/testmatrix_segments.csv create mode 100644 tests/data/testmatrix_segments_old.csv rename tests/{ => data}/testmatrix_semantic.csv (99%) rename tests/{ => data}/testmatrix_semantic_2.csv (100%) rename tests/{ => data}/testmatrix_sensitive.csv (100%) create mode 100644 tests/data/testmatrix_unicode.csv rename tests/{ => data}/testmatrix_variationId.csv (75%) delete mode 100644 tests/test-rules.json delete mode 100644 tests/test.json diff --git a/.github/workflows/php-ci.yml b/.github/workflows/php-ci.yml index 34f2307..cec7808 100644 --- a/.github/workflows/php-ci.yml +++ b/.github/workflows/php-ci.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php-versions: [ '8.1', '8.2' ] + php-versions: [ '8.1', '8.2', '8.3' ] steps: - uses: actions/checkout@v4 diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index abd507c..c145b02 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -1,11 +1,14 @@ in(__DIR__.'/src') ->in(__DIR__.'/tests') ; -$config = (new PhpCsFixer\Config()) +$config = (new Config()) ->setRules([ '@PhpCsFixer' => true, '@PSR2' => true, diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..aabea22 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,21 @@ +{ + // List of extensions which should be recommended for users of this workspace. + "recommendations": [ + // For intellisense and navigation in code: + "bmewburn.vscode-intelephense-client", + + // For formatting and linting: + "junstyle.php-cs-fixer", + "SanderRonde.phpstan-vscode", + + // For exploring and running tests: + "hbenl.vscode-test-explorer", + "recca0120.vscode-phpunit", + + // For debugging: + "xdebug.php-debug" // to make this work, you need xdebug installed - see the description of the extension + ], + + // List of extensions recommended by VS Code that should not be recommended for users of this workspace. + "unwantedRecommendations": [] +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..b4f58e4 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,23 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + // To debug tests, you need to launch this configuration first, then run the test to debug from the test explorer. + "name": "Listen for XDebug", + "type": "php", + "request": "launch", + "port": 9003 + }, + { + "name": "Launch currently open script", + "type": "php", + "request": "launch", + "program": "${file}", + "cwd": "${fileDirname}", + "port": 9003 + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9b4faa0 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,16 @@ +{ + "intelephense.environment.phpVersion": "8.1.0", + // On Linux, replace "phpdebug.bat" with "XDEBUG_MODE=debug php". + // To make debugging work, you also need xdebug.start_with_request=yes in php.ini! + "phpunit.php": "phpdebug.bat", + "phpunit.args": [ + "-c", + "phpunit.xml" + ], + "php-cs-fixer.executablePath": "php-cs-fixer", + "php-cs-fixer.executablePathWindows": "${workspaceFolder}/vendor/bin/php-cs-fixer.bat", + "[php]": { + "editor.defaultFormatter": "junstyle.php-cs-fixer" + }, + "phpstan.configFile": "phpstan.neon" +} \ No newline at end of file diff --git a/phpdebug.bat b/phpdebug.bat new file mode 100644 index 0000000..3771872 --- /dev/null +++ b/phpdebug.bat @@ -0,0 +1,4 @@ +@ECHO OFF +setlocal DISABLEDELAYEDEXPANSION +set XDEBUG_MODE=debug +php %* \ No newline at end of file diff --git a/samples/consolesample/composer.json b/samples/consolesample/composer.json index e05b8f8..63ba894 100644 --- a/samples/consolesample/composer.json +++ b/samples/consolesample/composer.json @@ -1,6 +1,6 @@ { "require": { - "configcat/configcat-client": "^6", + "configcat/configcat-client": "^9", "monolog/monolog": "^1.0" } } diff --git a/samples/laravel/composer.json b/samples/laravel/composer.json index 95e6cbf..d295722 100644 --- a/samples/laravel/composer.json +++ b/samples/laravel/composer.json @@ -9,7 +9,7 @@ "license": "MIT", "require": { "php": "^7.2.5", - "configcat/configcat-client": "^6", + "configcat/configcat-client": "^9", "fideloper/proxy": "^4.0", "laravel/framework": "^7.0", "laravel/tinker": "^2.0" diff --git a/src/Attributes/Config.php b/src/Attributes/Config.php deleted file mode 100644 index fc6c4c1..0000000 --- a/src/Attributes/Config.php +++ /dev/null @@ -1,14 +0,0 @@ -set($key, $value->serialize()); - } catch (Exception $exception) { + } catch (Throwable $exception) { $this->logger->error('Error occurred while writing the cache.', [ 'event_id' => 2201, 'exception' => $exception, ]); @@ -63,7 +63,7 @@ public function load(string $key): ConfigEntry } return ConfigEntry::fromCached($cached); - } catch (Exception $exception) { + } catch (Throwable $exception) { $this->logger->error('Error occurred while reading the cache.', [ 'event_id' => 2200, 'exception' => $exception, ]); diff --git a/src/Cache/ConfigEntry.php b/src/Cache/ConfigEntry.php index 539274a..ff452dc 100644 --- a/src/Cache/ConfigEntry.php +++ b/src/Cache/ConfigEntry.php @@ -4,6 +4,7 @@ namespace ConfigCat\Cache; +use ConfigCat\ConfigJson\Config; use UnexpectedValueException; /** @@ -79,10 +80,7 @@ public static function empty(): ConfigEntry public static function fromConfigJson(string $configJson, string $etag, float $fetchTime): ConfigEntry { - $deserialized = json_decode($configJson, true); - if (null == $deserialized) { - return self::empty(); - } + $deserialized = Config::deserialize($configJson); return new ConfigEntry($configJson, $deserialized, $etag, $fetchTime); } diff --git a/src/ConfigCatClient.php b/src/ConfigCatClient.php index 12f1819..1376435 100644 --- a/src/ConfigCatClient.php +++ b/src/ConfigCatClient.php @@ -4,13 +4,16 @@ namespace ConfigCat; -use ConfigCat\Attributes\Config; -use ConfigCat\Attributes\PercentageAttributes; -use ConfigCat\Attributes\RolloutAttributes; -use ConfigCat\Attributes\SettingAttributes; use ConfigCat\Cache\ArrayCache; use ConfigCat\Cache\ConfigCache; use ConfigCat\Cache\ConfigEntry; +use ConfigCat\ConfigJson\Config; +use ConfigCat\ConfigJson\PercentageOption; +use ConfigCat\ConfigJson\Setting; +use ConfigCat\ConfigJson\SettingType; +use ConfigCat\ConfigJson\SettingValue; +use ConfigCat\ConfigJson\SettingValueContainer; +use ConfigCat\ConfigJson\TargetingRule; use ConfigCat\Log\DefaultLogger; use ConfigCat\Log\InternalLogger; use ConfigCat\Log\LogLevel; @@ -19,13 +22,15 @@ use Exception; use InvalidArgumentException; use Psr\Log\LoggerInterface; +use stdClass; +use Throwable; /** * A client for handling configurations provided by ConfigCat. */ final class ConfigCatClient implements ClientInterface { - public const SDK_VERSION = '8.1.0'; + public const SDK_VERSION = '9.0.0'; private const CONFIG_JSON_CACHE_VERSION = 'v2'; private InternalLogger $logger; @@ -84,6 +89,18 @@ public function __construct(string $sdkKey, array $options = []) throw new InvalidArgumentException("'sdkKey' cannot be empty."); } + $overrides = (isset($options[ClientOptions::FLAG_OVERRIDES]) + && $options[ClientOptions::FLAG_OVERRIDES] instanceof FlagOverrides) + ? $options[ClientOptions::FLAG_OVERRIDES] + : null; + + if (OverrideBehaviour::LOCAL_ONLY !== $overrides?->getBehaviour()) { + $customBaseUrl = isset($options[ClientOptions::BASE_URL]) && !empty($options[ClientOptions::BASE_URL]); + if (!self::isValidSdkKey($sdkKey, $customBaseUrl)) { + throw new InvalidArgumentException("'sdkKey' is invalid."); + } + } + $this->hooks = new Hooks(); $this->cacheKey = sha1(sprintf('%s_'.ConfigFetcher::CONFIG_JSON_NAME.'_'.self::CONFIG_JSON_CACHE_VERSION, $sdkKey)); @@ -104,10 +121,7 @@ public function __construct(string $sdkKey, array $options = []) $this->logger = new InternalLogger($externalLogger, $logLevel, $exceptionsToIgnore, $this->hooks); - $this->overrides = (isset($options[ClientOptions::FLAG_OVERRIDES]) - && $options[ClientOptions::FLAG_OVERRIDES] instanceof FlagOverrides) - ? $options[ClientOptions::FLAG_OVERRIDES] - : null; + $this->overrides = $overrides; $this->defaultUser = (isset($options[ClientOptions::DEFAULT_USER]) && $options[ClientOptions::DEFAULT_USER] instanceof User) @@ -161,11 +175,12 @@ public function getValue(string $key, mixed $defaultValue, ?User $user = null): return $this->evaluate( $key, - $settingsResult->settings[$key], + $settingsResult->settings, + $defaultValue, $user, $settingsResult->fetchTime )->getValue(); - } catch (Exception $exception) { + } catch (Throwable $exception) { $message = "Error occurred in the `getValue` method while evaluating setting '".$key."'. ". 'Returning the `defaultValue` parameter that you specified '. "in your application: '".Utils::getStringRepresentation($defaultValue)."'."; @@ -177,7 +192,8 @@ public function getValue(string $key, mixed $defaultValue, ?User $user = null): $key, $defaultValue, $user, - InternalLogger::format($message, $messageCtx) + InternalLogger::format($message, $messageCtx), + $exception )); return $defaultValue; @@ -210,8 +226,8 @@ public function getValueDetails(string $key, mixed $defaultValue, ?User $user = return $details; } - return $this->evaluate($key, $settingsResult->settings[$key], $user, $settingsResult->fetchTime); - } catch (Exception $exception) { + return $this->evaluate($key, $settingsResult->settings, $defaultValue, $user, $settingsResult->fetchTime); + } catch (Throwable $exception) { $message = "Error occurred in the `getValueDetails` method while evaluating setting '".$key."'. ". 'Returning the `defaultValue` parameter that you specified in '. "your application: '".Utils::getStringRepresentation($defaultValue)."'."; @@ -219,7 +235,7 @@ public function getValueDetails(string $key, mixed $defaultValue, ?User $user = 'event_id' => 1002, 'exception' => $exception, ]; $this->logger->error($message, $messageCtx); - $details = EvaluationDetails::fromError($key, $defaultValue, $user, InternalLogger::format($message, $messageCtx)); + $details = EvaluationDetails::fromError($key, $defaultValue, $user, InternalLogger::format($message, $messageCtx), $exception); $this->hooks->fireOnFlagEvaluated($details); return $details; @@ -244,7 +260,7 @@ public function getKeyAndValue(string $variationId): ?Pair return empty($settingsResult->settings) ? null : $this->parseKeyAndValue($settingsResult->settings, $variationId); - } catch (Exception $exception) { + } catch (Throwable $exception) { $this->logger->error('Error occurred in the `getKeyAndValue` method. Returning null.', [ 'event_id' => 1002, 'exception' => $exception, ]); @@ -267,7 +283,7 @@ public function getAllKeys(): array } return empty($settingsResult->settings) ? [] : array_keys($settingsResult->settings); - } catch (Exception $exception) { + } catch (Throwable $exception) { $this->logger->error('Error occurred in the `getAllKeys` method. Returning empty array.', [ 'event_id' => 1002, 'exception' => $exception, ]); @@ -292,7 +308,7 @@ public function getAllValues(?User $user = null): array } return empty($settingsResult->settings) ? [] : $this->parseValues($settingsResult, $user); - } catch (Exception $exception) { + } catch (Throwable $exception) { $this->logger->error('Error occurred in the `getAllValues` method. Returning empty array.', [ 'event_id' => 1002, 'exception' => $exception, ]); @@ -321,14 +337,15 @@ public function getAllValueDetails(?User $user = null): array foreach ($keys as $key) { $result[$key] = $this->evaluate( $key, - $settingsResult->settings[$key], + $settingsResult->settings, + null, $user, $settingsResult->fetchTime ); } return $result; - } catch (Exception $exception) { + } catch (Throwable $exception) { $this->logger->error('Error occurred in the `getAllValueDetails` method. Returning empty array.', [ 'event_id' => 1002, 'exception' => $exception, ]); @@ -420,7 +437,7 @@ public function isOffline(): bool private function checkSettingsAvailable(SettingsResult $settingsResult, string $defaultReturnValue): bool { - if (empty($settingsResult->settings)) { + if (!$settingsResult->hasConfigJson) { $this->logger->error('Config JSON is not present. Returning '.$defaultReturnValue.'.', [ 'event_id' => 1000, ]); @@ -475,7 +492,8 @@ private function parseValues(SettingsResult $settingsResult, User $user = null): foreach ($keys as $key) { $result[$key] = $this->evaluate( $key, - $settingsResult->settings[$key], + $settingsResult->settings, + null, $user, $settingsResult->fetchTime )->getValue(); @@ -485,27 +503,25 @@ private function parseValues(SettingsResult $settingsResult, User $user = null): } /** - * @param mixed[] $setting + * @param array $settings */ - private function evaluate(string $key, array $setting, ?User $user, float $fetchTime): EvaluationDetails + private function evaluate(string $key, array $settings, mixed $defaultValue, ?User $user, float $fetchTime): EvaluationDetails { - $actualUser = null === $user ? $this->defaultUser : $user; - $collector = new EvaluationLogCollector(); - $collector->add('Evaluating '.$key.'.'); - $result = $this->evaluator->evaluate($key, $setting, $collector, $actualUser); - $this->logger->info((string) $collector, [ - 'event_id' => 5000, - ]); + $user ??= $this->defaultUser; + $evaluateContext = new EvaluateContext($key, $settings[$key], $user, $settings); + $returnValue = $defaultValue; + $evaluateResult = $this->evaluator->evaluate($defaultValue, $evaluateContext, $returnValue); $details = new EvaluationDetails( $key, - $result->variationId, - $result->value, - $actualUser, + $evaluateResult->selectedValue[SettingValueContainer::VARIATION_ID] ?? null, + $returnValue, + $user, false, null, + null, $fetchTime, - $result->targetingRule, - $result->percentageRule + $evaluateResult->matchedTargetingRule, + $evaluateResult->matchedPercentageOption ); $this->hooks->fireOnFlagEvaluated($details); @@ -513,27 +529,39 @@ private function evaluate(string $key, array $setting, ?User $user, float $fetch } /** - * @param array $json + * @param array $settings */ - private function parseKeyAndValue(array $json, string $variationId): ?Pair + private function parseKeyAndValue(array $settings, string $variationId): ?Pair { - foreach ($json as $key => $value) { - if ($variationId == $value[SettingAttributes::VARIATION_ID]) { - return new Pair($key, $value[SettingAttributes::VALUE]); - } + foreach ($settings as $key => $setting) { + /** @var SettingType|stdClass $settingType */ + $settingType = Setting::getType(Setting::ensure($setting)); - $rolloutRules = $value[SettingAttributes::ROLLOUT_RULES]; - $percentageItems = $value[SettingAttributes::ROLLOUT_PERCENTAGE_ITEMS]; + if ($variationId === ($setting[Setting::VARIATION_ID] ?? null)) { + return new Pair($key, SettingValue::get($setting[Setting::VALUE] ?? null, $settingType)); + } - foreach ($rolloutRules as $rolloutValue) { - if ($variationId == $rolloutValue[RolloutAttributes::VARIATION_ID]) { - return new Pair($key, $rolloutValue[RolloutAttributes::VALUE]); + $targetingRules = TargetingRule::ensureList($setting[Setting::TARGETING_RULES] ?? []); + foreach ($targetingRules as $targetingRule) { + if (TargetingRule::hasPercentageOptions(TargetingRule::ensure($targetingRule))) { + $percentageOptions = $targetingRule[TargetingRule::PERCENTAGE_OPTIONS]; + foreach ($percentageOptions as $percentageOption) { + if ($variationId === ($percentageOption[PercentageOption::VARIATION_ID] ?? null)) { + return new Pair($key, SettingValue::get($percentageOption[PercentageOption::VALUE] ?? null, $settingType)); + } + } + } else { + $simpleValue = $targetingRule[TargetingRule::SIMPLE_VALUE]; + if ($variationId === ($simpleValue[SettingValueContainer::VARIATION_ID] ?? null)) { + return new Pair($key, SettingValue::get($simpleValue[SettingValueContainer::VALUE] ?? null, $settingType)); + } } } - foreach ($percentageItems as $percentageValue) { - if ($variationId == $percentageValue[PercentageAttributes::VARIATION_ID]) { - return new Pair($key, $percentageValue[PercentageAttributes::VALUE]); + $percentageOptions = PercentageOption::ensureList($setting[Setting::PERCENTAGE_OPTIONS] ?? []); + foreach ($percentageOptions as $percentageOption) { + if ($variationId === ($percentageOption[PercentageOption::VARIATION_ID] ?? null)) { + return new Pair($key, SettingValue::get($percentageOption[PercentageOption::VALUE] ?? null, $settingType)); } } } @@ -556,13 +584,13 @@ private function getSettingsResult(): SettingsResult $local = $this->overrides->getDataSource()->getOverrides(); $remote = $this->getRemoteSettingsResult(); - return new SettingsResult(array_merge($remote->settings, $local), $remote->fetchTime, $remote->hasConfigJson); + return new SettingsResult(array_merge($remote->settings, $local), $remote->fetchTime, true); default: // remote over local $local = $this->overrides->getDataSource()->getOverrides(); $remote = $this->getRemoteSettingsResult(); - return new SettingsResult(array_merge($local, $remote->settings), $remote->fetchTime, $remote->hasConfigJson); + return new SettingsResult(array_merge($local, $remote->settings), $remote->fetchTime, true); } } @@ -581,13 +609,15 @@ private function getRemoteSettingsResult(): SettingsResult return new SettingsResult([], 0, false); } - return new SettingsResult($cacheEntry->getConfig()[Config::ENTRIES], $cacheEntry->getFetchTime(), true); + $settings = Setting::ensureMap($cacheEntry->getConfig()[Config::SETTINGS] ?? []); + + return new SettingsResult($settings, $cacheEntry->getFetchTime(), true); } private function handleResponse(FetchResponse $response, ConfigEntry $cacheEntry): ConfigEntry { if ($response->isFetched()) { - $this->hooks->fireOnConfigChanged($response->getConfigEntry()->getConfig()[Config::ENTRIES]); + $this->hooks->fireOnConfigChanged($response->getConfigEntry()->getConfig()[Config::SETTINGS] ?? []); $this->cache->store($this->cacheKey, $response->getConfigEntry()); return $response->getConfigEntry(); @@ -601,4 +631,23 @@ private function handleResponse(FetchResponse $response, ConfigEntry $cacheEntry return $cacheEntry; } + + private static function isValidSdkKey(string $sdkKey, bool $customBaseUrl): bool + { + $proxyPrefix = 'configcat-proxy/'; + if ($customBaseUrl && strlen($sdkKey) > strlen($proxyPrefix) && str_starts_with($sdkKey, $proxyPrefix)) { + return true; + } + + $components = explode('/', $sdkKey); + $keyLength = 22; + + switch (count($components)) { + case 2: return strlen($components[0]) === $keyLength && strlen($components[1]) === $keyLength; + + case 3: return 'configcat-sdk-1' === $components[0] && strlen($components[1]) === $keyLength && strlen($components[2]) === $keyLength; + + default: return false; + } + } } diff --git a/src/ConfigFetcher.php b/src/ConfigFetcher.php index 128d367..2fae365 100644 --- a/src/ConfigFetcher.php +++ b/src/ConfigFetcher.php @@ -4,14 +4,16 @@ namespace ConfigCat; -use ConfigCat\Attributes\Config; -use ConfigCat\Attributes\Preferences; use ConfigCat\Cache\ConfigEntry; +use ConfigCat\ConfigJson\Config; +use ConfigCat\ConfigJson\Preferences; +use ConfigCat\ConfigJson\RedirectMode; use ConfigCat\Http\FetchClientInterface; use ConfigCat\Http\GuzzleFetchClient; use ConfigCat\Log\InternalLogger; use InvalidArgumentException; use Psr\Http\Client\ClientExceptionInterface; +use UnexpectedValueException; /** * Class ConfigFetcher This class is used to fetch the latest configuration. @@ -21,15 +23,11 @@ final class ConfigFetcher { public const ETAG_HEADER = 'ETag'; - public const CONFIG_JSON_NAME = 'config_v5.json'; + public const CONFIG_JSON_NAME = 'config_v6.json'; public const GLOBAL_URL = 'https://cdn-global.configcat.com'; public const EU_ONLY_URL = 'https://cdn-eu.configcat.com'; - public const NO_REDIRECT = 0; - public const SHOULD_REDIRECT = 1; - public const FORCE_REDIRECT = 2; - private InternalLogger $logger; private string $urlPath; private string $baseUrl; @@ -97,24 +95,24 @@ private function executeFetch(?string $etag, string $url, int $executionCount): } $newUrl = ''; - if (isset($response->getConfigEntry()->getConfig()[Config::PREFERENCES][Preferences::BASE_URL])) { - $newUrl = $response->getConfigEntry()->getConfig()[Config::PREFERENCES][Preferences::BASE_URL]; + $preferences = $response->getConfigEntry()->getConfig()[Config::PREFERENCES]; + if (isset($preferences[Preferences::BASE_URL])) { + $newUrl = $preferences[Preferences::BASE_URL]; } if (empty($newUrl) || $newUrl == $url) { return $response; } - $preferences = $response->getConfigEntry()->getConfig()[Config::PREFERENCES]; - $redirect = $preferences[Preferences::REDIRECT]; - if ($this->urlIsCustom && self::FORCE_REDIRECT != $redirect) { + $redirect = RedirectMode::from($preferences[Preferences::REDIRECT_MODE] ?? RedirectMode::NO->value); + if ($this->urlIsCustom && RedirectMode::FORCE != $redirect) { return $response; } - if (self::NO_REDIRECT == $redirect) { + if (RedirectMode::NO == $redirect) { return $response; } - if (self::SHOULD_REDIRECT == $redirect) { + if (RedirectMode::SHOULD == $redirect) { $this->logger->warning( 'The `dataGovernance` parameter specified at the client initialization is '. 'not in sync with the preferences on the ConfigCat Dashboard. '. @@ -159,11 +157,13 @@ private function sendConfigFetchRequest(?string $etag, string $url): FetchRespon if ($statusCode >= 200 && $statusCode < 300) { $this->logger->debug('Fetch was successful: new config fetched.'); - $entry = ConfigEntry::fromConfigJson((string) $response->getBody(), $etag ?? '', Utils::getUnixMilliseconds()); - if (JSON_ERROR_NONE !== json_last_error()) { - $message = 'Fetching config JSON was successful but the HTTP response content was invalid. JSON error: '.json_last_error_msg(); + try { + $entry = ConfigEntry::fromConfigJson((string) $response->getBody(), $etag ?? '', Utils::getUnixMilliseconds()); + } catch (UnexpectedValueException $ex) { + $message = 'Fetching config JSON was successful but the HTTP response content was invalid.'; $messageCtx = [ 'event_id' => 1105, + 'exception' => $ex, ]; $this->logger->error($message, $messageCtx); @@ -187,7 +187,9 @@ private function sendConfigFetchRequest(?string $etag, string $url): FetchRespon return FetchResponse::failure(InternalLogger::format($message, $messageCtx)); } catch (ClientExceptionInterface $exception) { - $message = 'Unexpected error occurred while trying to fetch config JSON.'; + $message = '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.'; $messageCtx = ['event_id' => 1103, 'exception' => $exception]; $this->logger->error($message, $messageCtx); diff --git a/src/ConfigJson/ConditionContainer.php b/src/ConfigJson/ConditionContainer.php new file mode 100644 index 0000000..6c1a523 --- /dev/null +++ b/src/ConfigJson/ConditionContainer.php @@ -0,0 +1,62 @@ +> + * + * @throws UnexpectedValueException + * + * @internal + */ + public static function ensureList(mixed $conditions): array + { + if (!is_array($conditions) || !array_is_list($conditions)) { + throw new UnexpectedValueException('Condition list is invalid.'); + } + + return $conditions; + } + + /** + * @return array + * + * @throws UnexpectedValueException + * + * @internal + */ + public static function ensure(mixed $condition): array + { + if (!is_array($condition)) { + throw new UnexpectedValueException('Condition is missing or invalid.'); + } + + return $condition; + } + + /** + * @return callable(array, string&): (null|array) + */ + public static function conditionAccessor(): callable + { + return function (array $conditionContainer, string &$conditionType): ?array { + return $conditionContainer[$conditionType = ConditionContainer::USER_CONDITION] + ?? $conditionContainer[$conditionType = ConditionContainer::PREREQUISITE_FLAG_CONDITION] + ?? $conditionContainer[$conditionType = ConditionContainer::SEGMENT_CONDITION] + ?? null; + }; + } +} diff --git a/src/ConfigJson/Config.php b/src/ConfigJson/Config.php new file mode 100644 index 0000000..2b5b36b --- /dev/null +++ b/src/ConfigJson/Config.php @@ -0,0 +1,68 @@ + $config + * + * @internal + */ + public static function fixupSaltAndSegments(array &$config): void + { + $settings = &$config[self::SETTINGS] ?? []; + if (is_array($settings) && !empty($settings)) { + $salt = $config[self::PREFERENCES][Preferences::SALT] ?? null; + $segments = $config[self::SEGMENTS] ?? []; + + foreach ($settings as &$setting) { + $setting[Setting::CONFIG_JSON_SALT] = $salt; + $setting[Setting::CONFIG_SEGMENTS] = $segments; + } + } + } + + /** + * @return array + * + * @throws UnexpectedValueException + */ + public static function deserialize(string $json): array + { + $config = json_decode($json, true); + if (JSON_ERROR_NONE !== json_last_error()) { + throw new UnexpectedValueException('JSON error: '.json_last_error_msg()); + } + + if (!is_array($config)) { + throw new UnexpectedValueException('Invalid config JSON content: '.$json); + } + + self::fixupSaltAndSegments($config); + + return $config; + } +} diff --git a/src/ConfigJson/PercentageOption.php b/src/ConfigJson/PercentageOption.php new file mode 100644 index 0000000..a1d9547 --- /dev/null +++ b/src/ConfigJson/PercentageOption.php @@ -0,0 +1,47 @@ +> + * + * @throws UnexpectedValueException + * + * @internal + */ + public static function ensureList(mixed $percentageOptions): array + { + if (!is_array($percentageOptions) || !array_is_list($percentageOptions)) { + throw new UnexpectedValueException('Percentage option list is invalid.'); + } + + return $percentageOptions; + } + + /** + * @return array + * + * @throws UnexpectedValueException + * + * @internal + */ + public static function ensure(mixed $percentageOption): array + { + if (!is_array($percentageOption)) { + throw new UnexpectedValueException('Percentage option is missing or invalid.'); + } + + return $percentageOption; + } +} diff --git a/src/ConfigJson/Preferences.php b/src/ConfigJson/Preferences.php new file mode 100644 index 0000000..7c4e8f2 --- /dev/null +++ b/src/ConfigJson/Preferences.php @@ -0,0 +1,26 @@ +> + * + * @throws UnexpectedValueException + * + * @internal + */ + public static function ensureList(mixed $segments): array + { + if (!is_array($segments) || !array_is_list($segments)) { + throw new UnexpectedValueException('Segment list is invalid.'); + } + + return $segments; + } + + /** + * @return array + * + * @throws UnexpectedValueException + * + * @internal + */ + public static function ensure(mixed $segment): array + { + if (!is_array($segment)) { + throw new UnexpectedValueException('Segment is missing or invalid.'); + } + + return $segment; + } + + /** + * @return callable(array, string&): array + */ + public static function conditionAccessor(): callable + { + return function (array $condition, string &$conditionType): array { + $conditionType = ConditionContainer::USER_CONDITION; + + return $condition; + }; + } +} diff --git a/src/ConfigJson/SegmentComparator.php b/src/ConfigJson/SegmentComparator.php new file mode 100644 index 0000000..59e142f --- /dev/null +++ b/src/ConfigJson/SegmentComparator.php @@ -0,0 +1,17 @@ + + * + * @throws UnexpectedValueException + * + * @internal + */ + public static function ensureMap(mixed $settings): array + { + if (!is_array($settings)) { + throw new UnexpectedValueException('Setting map is invalid.'); + } + + return $settings; + } + + /** + * @return array + * + * @throws UnexpectedValueException + * + * @internal + */ + public static function ensure(mixed $setting): array + { + if (!is_array($setting)) { + throw new UnexpectedValueException('Setting is missing or invalid.'); + } + + return $setting; + } + + /** + * @param array $setting + * + * @internal + */ + public static function getType(array $setting, bool $throwIfInvalid = true): null|SettingType|stdClass + { + $settingType = $setting[self::TYPE] ?? null; + if ($settingType === self::unsupportedTypeToken()) { + return $settingType; + } + + $settingType = SettingType::tryFrom($settingType); + if (isset($settingType)) { + return $settingType; + } + + if ($throwIfInvalid) { + throw new UnexpectedValueException('Setting type is missing or invalid.'); + } + + return null; + } + + /** + * @return array + * + * @internal + */ + public static function fromValue(mixed $value): array + { + if (is_bool($value)) { + $settingType = SettingType::BOOLEAN->value; + $value = [SettingValue::BOOLEAN => $value]; + } elseif (is_string($value)) { + $settingType = SettingType::STRING->value; + $value = [SettingValue::STRING => $value]; + } elseif (is_int($value)) { + $settingType = SettingType::INT->value; + $value = [SettingValue::INT => $value]; + } elseif (is_double($value)) { + $settingType = SettingType::DOUBLE->value; + $value = [SettingValue::DOUBLE => $value]; + } else { + $settingType = self::unsupportedTypeToken(); + } + + return [ + self::TYPE => $settingType, + self::VALUE => $value, + ]; + } + + /** + * Returns a token object for indicating an unsupported value coming from flag overrides. + */ + private static function unsupportedTypeToken(): stdClass + { + static $unsupportedTypeToken = null; + + return $unsupportedTypeToken ??= new stdClass(); + } +} diff --git a/src/ConfigJson/SettingType.php b/src/ConfigJson/SettingType.php new file mode 100644 index 0000000..91f49f1 --- /dev/null +++ b/src/ConfigJson/SettingType.php @@ -0,0 +1,23 @@ +> + * + * @throws UnexpectedValueException + * + * @internal + */ + public static function ensureList(mixed $targetingRules): array + { + if (!is_array($targetingRules) || !array_is_list($targetingRules)) { + throw new UnexpectedValueException('Targeting rule list is invalid.'); + } + + return $targetingRules; + } + + /** + * @return array + * + * @throws UnexpectedValueException + * + * @internal + */ + public static function ensure(mixed $targetingRule): array + { + if (!is_array($targetingRule)) { + throw new UnexpectedValueException('Targeting rule is missing or invalid.'); + } + + return $targetingRule; + } + + /** + * @param array $targetingRule + * + * @throws UnexpectedValueException + * + * @internal + */ + public static function hasPercentageOptions(array $targetingRule, bool $throwIfInvalid = true): ?bool + { + $simpleValue = $targetingRule[self::SIMPLE_VALUE] ?? null; + $percentageOptions = $targetingRule[self::PERCENTAGE_OPTIONS] ?? null; + + if (isset($simpleValue)) { + if (!isset($percentageOptions) && is_array($simpleValue)) { + return false; + } + } elseif (is_array($percentageOptions) && array_is_list($percentageOptions) && count($percentageOptions) > 0) { + return true; + } + + if ($throwIfInvalid) { + throw new UnexpectedValueException('Targeting rule THEN part is missing or invalid.'); + } + + return null; + } +} diff --git a/src/ConfigJson/UserComparator.php b/src/ConfigJson/UserComparator.php new file mode 100644 index 0000000..cb5c6a0 --- /dev/null +++ b/src/ConfigJson/UserComparator.php @@ -0,0 +1,119 @@ +Unix Epoch is less than the comparison value. */ + case DATETIME_BEFORE = 18; + + /** AFTER (UTC datetime) - It matches when the comparison attribute interpreted as the seconds elapsed since Unix Epoch is greater than the comparison value. */ + case DATETIME_AFTER = 19; + + /** EQUALS (hashed) - It matches when the comparison attribute is equal to the comparison value (where the comparison is performed using the salted SHA256 hashes of the values). */ + case SENSITIVE_TEXT_EQUALS = 20; + + /** NOT EQUALS (hashed) - It matches when the comparison attribute is not equal to the comparison value (where the comparison is performed using the salted SHA256 hashes of the values). */ + case SENSITIVE_TEXT_NOT_EQUALS = 21; + + /** STARTS WITH ANY OF (hashed) - It matches when the comparison attribute starts with any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). */ + case SENSITIVE_TEXT_STARTS_WITH_ANY_OF = 22; + + /** NOT STARTS WITH ANY OF (hashed) - It matches when the comparison attribute does not start with any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). */ + case SENSITIVE_TEXT_NOT_STARTS_WITH_ANY_OF = 23; + + /** ENDS WITH ANY OF (hashed) - It matches when the comparison attribute ends with any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). */ + case SENSITIVE_TEXT_ENDS_WITH_ANY_OF = 24; + + /** NOT ENDS WITH ANY OF (hashed) - It matches when the comparison attribute does not end with any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). */ + case SENSITIVE_TEXT_NOT_ENDS_WITH_ANY_OF = 25; + + /** ARRAY CONTAINS ANY OF (hashed) - It matches when the comparison attribute interpreted as a comma-separated list contains any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). */ + case SENSITIVE_ARRAY_CONTAINS_ANY_OF = 26; + + /** ARRAY NOT CONTAINS ANY OF (hashed) - It matches when the comparison attribute interpreted as a comma-separated list does not contain any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). */ + case SENSITIVE_ARRAY_NOT_CONTAINS_ANY_OF = 27; + + /** EQUALS (cleartext) - It matches when the comparison attribute is equal to the comparison value. */ + case TEXT_EQUALS = 28; + + /** NOT EQUALS (cleartext) - It matches when the comparison attribute is not equal to the comparison value. */ + case TEXT_NOT_EQUALS = 29; + + /** STARTS WITH ANY OF (cleartext) - It matches when the comparison attribute starts with any of the comparison values. */ + case TEXT_STARTS_WITH_ANY_OF = 30; + + /** NOT STARTS WITH ANY OF (cleartext) - It matches when the comparison attribute does not start with any of the comparison values. */ + case TEXT_NOT_STARTS_WITH_ANY_OF = 31; + + /** ENDS WITH ANY OF (cleartext) - It matches when the comparison attribute ends with any of the comparison values. */ + case TEXT_ENDS_WITH_ANY_OF = 32; + + /** NOT ENDS WITH ANY OF (cleartext) - It matches when the comparison attribute does not end with any of the comparison values. */ + case TEXT_NOT_ENDS_WITH_ANY_OF = 33; + + /** ARRAY CONTAINS ANY OF (cleartext) - It matches when the comparison attribute interpreted as a comma-separated list contains any of the comparison values. */ + case ARRAY_CONTAINS_ANY_OF = 34; + + /** ARRAY NOT CONTAINS ANY OF (cleartext) - It matches when the comparison attribute interpreted as a comma-separated list does not contain any of the comparison values. */ + case ARRAY_NOT_CONTAINS_ANY_OF = 35; +} diff --git a/src/ConfigJson/UserCondition.php b/src/ConfigJson/UserCondition.php new file mode 100644 index 0000000..43a4864 --- /dev/null +++ b/src/ConfigJson/UserCondition.php @@ -0,0 +1,17 @@ +'; + public const INVALID_REFERENCE_PLACEHOLDER = ''; + public const INVALID_OPERATOR_PLACEHOLDER = ''; + public const INVALID_VALUE_PLACEHOLDER = ''; + + private const VALUE_TEXT = 'value'; + private const VALUES_TEXT = 'values'; + + private const STRING_LIST_MAX_COUNT = 10; + + private string $log = ''; + private string $indent = ''; + + public function __toString(): string + { + return $this->log; + } + + public function resetIndent(): self + { + $this->indent = ''; + + return $this; + } + + public function increaseIndent(): self + { + $this->indent .= ' '; + + return $this; + } + + public function decreaseIndent(): self + { + $this->indent = substr($this->indent, 2); + + return $this; + } + + public function newLine(string $text = ''): self + { + $this->log .= PHP_EOL.$this->indent.$text; + + return $this; + } + + public function append(string $text): self + { + $this->log .= $text; + + return $this; + } + + /** + * @param array $condition + */ + public function appendUserCondition(array $condition): self + { + $comparisonAttribute = $condition[UserCondition::COMPARISON_ATTRIBUTE] ?? null; + if (!is_string($comparisonAttribute)) { + $comparisonAttribute = self::INVALID_NAME_PLACEHOLDER; + } + + $comparator = UserComparator::tryFrom($condition[UserCondition::COMPARATOR] ?? null); + + switch ($comparator) { + case UserComparator::TEXT_IS_ONE_OF: + case UserComparator::TEXT_IS_NOT_ONE_OF: + case UserComparator::TEXT_CONTAINS_ANY_OF: + case UserComparator::TEXT_NOT_CONTAINS_ANY_OF: + case UserComparator::SEMVER_IS_ONE_OF: + case UserComparator::SEMVER_IS_NOT_ONE_OF: + case UserComparator::TEXT_STARTS_WITH_ANY_OF: + case UserComparator::TEXT_NOT_STARTS_WITH_ANY_OF: + case UserComparator::TEXT_ENDS_WITH_ANY_OF: + case UserComparator::TEXT_NOT_ENDS_WITH_ANY_OF: + case UserComparator::ARRAY_CONTAINS_ANY_OF: + case UserComparator::ARRAY_NOT_CONTAINS_ANY_OF: + return $this->appendUserConditionStringList( + $comparisonAttribute, + $comparator, + $condition[UserCondition::STRINGLIST_COMPARISON_VALUE] ?? null, + false + ); + + case UserComparator::SEMVER_LESS: + case UserComparator::SEMVER_LESS_OR_EQUALS: + case UserComparator::SEMVER_GREATER: + case UserComparator::SEMVER_GREATER_OR_EQUALS: + case UserComparator::TEXT_EQUALS: + case UserComparator::TEXT_NOT_EQUALS: + return $this->appendUserConditionString( + $comparisonAttribute, + $comparator, + $condition[UserCondition::STRING_COMPARISON_VALUE] ?? null, + false + ); + + case UserComparator::NUMBER_EQUALS: + case UserComparator::NUMBER_NOT_EQUALS: + case UserComparator::NUMBER_LESS: + case UserComparator::NUMBER_LESS_OR_EQUALS: + case UserComparator::NUMBER_GREATER: + case UserComparator::NUMBER_GREATER_OR_EQUALS: + return $this->appendUserConditionNumber( + $comparisonAttribute, + $comparator, + $condition[UserCondition::NUMBER_COMPARISON_VALUE] ?? null + ); + + case UserComparator::SENSITIVE_TEXT_IS_ONE_OF: + case UserComparator::SENSITIVE_TEXT_IS_NOT_ONE_OF: + case UserComparator::SENSITIVE_TEXT_STARTS_WITH_ANY_OF: + case UserComparator::SENSITIVE_TEXT_NOT_STARTS_WITH_ANY_OF: + case UserComparator::SENSITIVE_TEXT_ENDS_WITH_ANY_OF: + case UserComparator::SENSITIVE_TEXT_NOT_ENDS_WITH_ANY_OF: + case UserComparator::SENSITIVE_ARRAY_CONTAINS_ANY_OF: + case UserComparator::SENSITIVE_ARRAY_NOT_CONTAINS_ANY_OF: + return $this->appendUserConditionStringList( + $comparisonAttribute, + $comparator, + $condition[UserCondition::STRINGLIST_COMPARISON_VALUE] ?? null, + true + ); + + case UserComparator::DATETIME_BEFORE: + case UserComparator::DATETIME_AFTER: + return $this->appendUserConditionNumber( + $comparisonAttribute, + $comparator, + $condition[UserCondition::NUMBER_COMPARISON_VALUE] ?? null, + true + ); + + case UserComparator::SENSITIVE_TEXT_EQUALS: + case UserComparator::SENSITIVE_TEXT_NOT_EQUALS: + return $this->appendUserConditionString( + $comparisonAttribute, + $comparator, + $condition[UserCondition::STRING_COMPARISON_VALUE] ?? null, + true + ); + + default: + $comparisonValue = $condition[UserCondition::STRING_COMPARISON_VALUE] + ?? $condition[UserCondition::NUMBER_COMPARISON_VALUE] + ?? $condition[UserCondition::STRINGLIST_COMPARISON_VALUE] + ?? null; + + $comparisonValue = is_string($comparisonValue) || is_double($comparisonValue) || is_int($comparisonValue) || Utils::isStringList($comparisonValue) + ? Utils::getStringRepresentation($comparisonValue) + : null; + + return $this->appendUserConditionCore($comparisonAttribute, $comparator, $comparisonValue); + } + } + + /** + * @param array $condition + * @param array $settings + */ + public function appendPrerequisiteFlagCondition(array $condition, array $settings): self + { + $prerequisiteFlagKey = $condition[PrerequisiteFlagCondition::PREREQUISITE_FLAG_KEY] ?? null; + if (!is_string($prerequisiteFlagKey)) { + $prerequisiteFlagKey = self::INVALID_NAME_PLACEHOLDER; + } elseif (!array_key_exists($prerequisiteFlagKey, $settings)) { + $prerequisiteFlagKey = self::INVALID_REFERENCE_PLACEHOLDER; + } + + $comparator = PrerequisiteFlagComparator::tryFrom($condition[PrerequisiteFlagCondition::COMPARATOR] ?? null); + $comparatorFormatted = self::formatPrerequisiteFlagComparator($comparator); + + $comparisonValue = SettingValue::infer($condition[PrerequisiteFlagCondition::COMPARISON_VALUE] ?? null); + $comparisonValueFormatted = self::formatSettingValue($comparisonValue); + + return $this->append("Flag '{$prerequisiteFlagKey}' {$comparatorFormatted} '{$comparisonValueFormatted}'"); + } + + /** + * @param array $condition + */ + public function appendSegmentCondition(array $condition, mixed $segments): self + { + $segmentIndex = $condition[SegmentCondition::SEGMENT_INDEX] ?? null; + $segment = $segments[is_int($segmentIndex) ? $segmentIndex : null] ?? null; + if (isset($segment)) { + $segmentName = $segment[Segment::NAME] ?? null; + if (!is_string($segmentName) || '' === $segmentName) { + $segmentName = self::INVALID_NAME_PLACEHOLDER; + } + } else { + $segmentName = self::INVALID_REFERENCE_PLACEHOLDER; + } + + $comparator = SegmentComparator::tryFrom($condition[SegmentCondition::COMPARATOR] ?? null); + $comparatorFormatted = self::formatSegmentComparator($comparator); + + return $this->append("User {$comparatorFormatted} '{$segmentName}'"); + } + + public function appendConditionResult(bool $result): self + { + return $this->append($result ? 'true' : 'false'); + } + + public function appendConditionConsequence(bool $result): self + { + $this->append(' => ')->appendConditionResult($result); + + return $result ? $this : $this->append(', skipping the remaining AND conditions'); + } + + /** + * @param array $targetingRule + */ + public function appendTargetingRuleConsequence(array $targetingRule, SettingType|stdClass $settingType, bool|string $isMatchOrError, bool $newLine): self + { + $this->increaseIndent(); + + $this->appendTargetingRuleThenPart($targetingRule, $settingType, $newLine) + ->append(' => ')->append(true === $isMatchOrError ? 'MATCH, applying rule' : (false === $isMatchOrError ? 'no match' : $isMatchOrError)) + ; + + return $this->decreaseIndent(); + } + + public static function formatSettingValue(mixed $value): string + { + if (!isset($value)) { + return self::INVALID_VALUE_PLACEHOLDER; + } + + return Utils::getStringRepresentation($value); + } + + public static function formatUserComparator(?UserComparator $comparator): string + { + switch ($comparator) { + case UserComparator::TEXT_IS_ONE_OF: + case UserComparator::SENSITIVE_TEXT_IS_ONE_OF: + case UserComparator::SEMVER_IS_ONE_OF: return 'IS ONE OF'; + + case UserComparator::TEXT_IS_NOT_ONE_OF: + case UserComparator::SENSITIVE_TEXT_IS_NOT_ONE_OF: + case UserComparator::SEMVER_IS_NOT_ONE_OF: return 'IS NOT ONE OF'; + + case UserComparator::TEXT_CONTAINS_ANY_OF: return 'CONTAINS ANY OF'; + + case UserComparator::TEXT_NOT_CONTAINS_ANY_OF: return 'NOT CONTAINS ANY OF'; + + case UserComparator::SEMVER_LESS: + case UserComparator::NUMBER_LESS: return '<'; + + case UserComparator::SEMVER_LESS_OR_EQUALS: + case UserComparator::NUMBER_LESS_OR_EQUALS: return '<='; + + case UserComparator::SEMVER_GREATER: + case UserComparator::NUMBER_GREATER: return '>'; + + case UserComparator::SEMVER_GREATER_OR_EQUALS: + case UserComparator::NUMBER_GREATER_OR_EQUALS: return '>='; + + case UserComparator::NUMBER_EQUALS: return '='; + + case UserComparator::NUMBER_NOT_EQUALS: return '!='; + + case UserComparator::DATETIME_BEFORE: return 'BEFORE'; + + case UserComparator::DATETIME_AFTER: return 'AFTER'; + + case UserComparator::TEXT_EQUALS: + case UserComparator::SENSITIVE_TEXT_EQUALS: return 'EQUALS'; + + case UserComparator::TEXT_NOT_EQUALS: + case UserComparator::SENSITIVE_TEXT_NOT_EQUALS: return 'NOT EQUALS'; + + case UserComparator::TEXT_STARTS_WITH_ANY_OF: + case UserComparator::SENSITIVE_TEXT_STARTS_WITH_ANY_OF: return 'STARTS WITH ANY OF'; + + case UserComparator::TEXT_NOT_STARTS_WITH_ANY_OF: + case UserComparator::SENSITIVE_TEXT_NOT_STARTS_WITH_ANY_OF: return 'NOT STARTS WITH ANY OF'; + + case UserComparator::TEXT_ENDS_WITH_ANY_OF: + case UserComparator::SENSITIVE_TEXT_ENDS_WITH_ANY_OF: return 'ENDS WITH ANY OF'; + + case UserComparator::TEXT_NOT_ENDS_WITH_ANY_OF: + case UserComparator::SENSITIVE_TEXT_NOT_ENDS_WITH_ANY_OF: return 'NOT ENDS WITH ANY OF'; + + case UserComparator::ARRAY_CONTAINS_ANY_OF: + case UserComparator::SENSITIVE_ARRAY_CONTAINS_ANY_OF: return 'ARRAY CONTAINS ANY OF'; + + case UserComparator::ARRAY_NOT_CONTAINS_ANY_OF: + case UserComparator::SENSITIVE_ARRAY_NOT_CONTAINS_ANY_OF: return 'ARRAY NOT CONTAINS ANY OF'; + + default: return self::INVALID_OPERATOR_PLACEHOLDER; + } + } + + public static function formatPrerequisiteFlagComparator(?PrerequisiteFlagComparator $comparator): string + { + switch ($comparator) { + case PrerequisiteFlagComparator::EQUALS: return 'EQUALS'; + + case PrerequisiteFlagComparator::NOT_EQUALS: return 'NOT EQUALS'; + + default: return self::INVALID_OPERATOR_PLACEHOLDER; + } + } + + public static function formatSegmentComparator(?SegmentComparator $comparator): string + { + switch ($comparator) { + case SegmentComparator::IS_IN: return 'IS IN SEGMENT'; + + case SegmentComparator::IS_NOT_IN: return 'IS NOT IN SEGMENT'; + + default: return self::INVALID_OPERATOR_PLACEHOLDER; + } + } + + /** + * @param array $condition + */ + public static function formatUserCondition(array $condition): string + { + $logBuilder = new self(); + + return (string) $logBuilder->appendUserCondition($condition); + } + + private function appendUserConditionCore(string $comparisonAttribute, ?UserComparator $comparator, ?string $comparisonValue): self + { + $comparatorFormatted = self::formatUserComparator($comparator); + $comparisonValue ??= self::INVALID_VALUE_PLACEHOLDER; + + return $this->append("User.{$comparisonAttribute} {$comparatorFormatted} '{$comparisonValue}'"); + } + + private function appendUserConditionString(string $comparisonAttribute, ?UserComparator $comparator, mixed $comparisonValue, bool $isSensitive): self + { + if (!is_string($comparisonValue)) { + return $this->appendUserConditionCore($comparisonAttribute, $comparator, null); + } + + return $this->appendUserConditionCore($comparisonAttribute, $comparator, !$isSensitive ? $comparisonValue : ''); + } + + private function appendUserConditionStringList(string $comparisonAttribute, ?UserComparator $comparator, mixed $comparisonValue, bool $isSensitive): self + { + if (!Utils::isStringList($comparisonValue)) { + return $this->appendUserConditionCore($comparisonAttribute, $comparator, null); + } + + $comparatorFormatted = self::formatUserComparator($comparator); + if ($isSensitive) { + $comparisonValueCount = count($comparisonValue); + $valueText = 1 === $comparisonValueCount ? self::VALUE_TEXT : self::VALUES_TEXT; + + return $this->append("User.{$comparisonAttribute} {$comparatorFormatted} [<{$comparisonValueCount} hashed {$valueText}>]"); + } + + $comparisonValueFormatted = Utils::formatStringList($comparisonValue, self::STRING_LIST_MAX_COUNT, function ($count) { + $valueText = 1 === $count ? self::VALUE_TEXT : self::VALUES_TEXT; + + return ", ... <{$count} more {$valueText}>"; + }); + + return $this->append("User.{$comparisonAttribute} {$comparatorFormatted} [{$comparisonValueFormatted}]"); + } + + private function appendUserConditionNumber(string $comparisonAttribute, ?UserComparator $comparator, mixed $comparisonValue, bool $isDateTime = false): self + { + if (!(is_double($comparisonValue) || is_int($comparisonValue))) { + return $this->appendUserConditionCore($comparisonAttribute, $comparator, null); + } + + $comparatorFormatted = self::formatUserComparator($comparator); + + if ($isDateTime && ($dateTime = Utils::dateTimeFromUnixSeconds($comparisonValue))) { + $dateIsoString = $dateTime->format('Y-m-d\\TH:i:s.vp'); + + return $this->append("User.{$comparisonAttribute} {$comparatorFormatted} '{$comparisonValue}' ({$dateIsoString} UTC)"); + } + + return $this->append("User.{$comparisonAttribute} {$comparatorFormatted} '{$comparisonValue}'"); + } + + /** + * @param array $targetingRule + */ + private function appendTargetingRuleThenPart(array $targetingRule, SettingType|stdClass $settingType, bool $newLine): self + { + ($newLine ? $this->newLine() : $this->append(' ')) + ->append('THEN') + ; + + if (!TargetingRule::hasPercentageOptions($targetingRule, false)) { + $simpleValue = SettingValue::get($targetingRule[TargetingRule::SIMPLE_VALUE][SettingValueContainer::VALUE] ?? null, $settingType, false); + $simpleValueFormatted = self::formatSettingValue($simpleValue); + + return $this->append(" '{$simpleValueFormatted}'"); + } + + return $this->append(' % options'); + } +} diff --git a/src/EvaluationDetails.php b/src/EvaluationDetails.php index 9211b36..f9c779d 100644 --- a/src/EvaluationDetails.php +++ b/src/EvaluationDetails.php @@ -4,11 +4,13 @@ namespace ConfigCat; +use Throwable; + class EvaluationDetails { /** - * @param null|mixed[] $matchedEvaluationRule - * @param null|mixed[] $matchedEvaluationPercentageRule + * @param null|array $matchedTargetingRule + * @param null|array $matchedPercentageOption * * @internal */ @@ -18,16 +20,17 @@ public function __construct( private readonly mixed $value, private readonly ?User $user, private readonly bool $isDefaultValue, - private readonly ?string $error, + private readonly ?string $errorMessage, + private readonly ?Throwable $errorException, private readonly float $fetchTimeUnixMilliseconds, - private readonly ?array $matchedEvaluationRule, - private readonly ?array $matchedEvaluationPercentageRule + private readonly ?array $matchedTargetingRule, + private readonly ?array $matchedPercentageOption ) {} /** * @internal */ - public static function fromError(string $key, mixed $value, ?User $user, ?string $error): EvaluationDetails + public static function fromError(string $key, mixed $value, ?User $user, string $errorMessage, ?Throwable $errorException = null): EvaluationDetails { return new EvaluationDetails( $key, @@ -35,7 +38,8 @@ public static function fromError(string $key, mixed $value, ?User $user, ?string $value, $user, true, - $error, + $errorMessage, + $errorException, 0, null, null @@ -83,11 +87,19 @@ public function isDefaultValue(): bool } /** - * @return ?string in case of an error, the error message + * @return ?string error message in case evaluation failed + */ + public function getErrorMessage(): ?string + { + return $this->errorMessage; + } + + /** + * @return ?Throwable the `Throwable` object related to the error in case evaluation failed (if any) */ - public function getError(): ?string + public function getErrorException(): ?Throwable { - return $this->error; + return $this->errorException; } /** @@ -99,18 +111,18 @@ public function getFetchTimeUnixMilliseconds(): float } /** - * @return null|mixed[] the targeting rule the evaluation was based on + * @return null|array the targeting rule (if any) that matched during the evaluation and was used to return the evaluated value */ - public function getMatchedEvaluationRule(): ?array + public function getMatchedTargetingRule(): ?array { - return $this->matchedEvaluationRule; + return $this->matchedTargetingRule; } /** - * @return null|mixed[] the percentage rule the evaluation was based on + * @return null|array the percentage option (if any) that was used to select the evaluated value */ - public function getMatchedEvaluationPercentageRule(): ?array + public function getMatchedPercentageOption(): ?array { - return $this->matchedEvaluationPercentageRule; + return $this->matchedPercentageOption; } } diff --git a/src/EvaluationLogCollector.php b/src/EvaluationLogCollector.php deleted file mode 100644 index c60490a..0000000 --- a/src/EvaluationLogCollector.php +++ /dev/null @@ -1,26 +0,0 @@ -entries); - } - - public function add(string $entry): void - { - $this->entries[] = $entry; - } -} diff --git a/src/Log/DefaultLogger.php b/src/Log/DefaultLogger.php index facc05c..df246a7 100644 --- a/src/Log/DefaultLogger.php +++ b/src/Log/DefaultLogger.php @@ -64,9 +64,13 @@ private static function logMsg(int $level, string|Stringable $message, array $co $context['timestamp'] = $date->format('Y-m-d\\TH:i:sP'); $context['level'] = LogLevel::asString($level); - $final = '[{timestamp}] ConfigCat.{level}: '.$message; + $final = self::interpolate('[{timestamp}] ConfigCat.{level}: '.$message, $context); - error_log(self::interpolate($final, $context)); + if (isset($context['exception'])) { + $final .= PHP_EOL.$context['exception']; + } + + error_log($final); } /** diff --git a/src/Log/InternalLogger.php b/src/Log/InternalLogger.php index c391ca1..92bf9b9 100644 --- a/src/Log/InternalLogger.php +++ b/src/Log/InternalLogger.php @@ -14,7 +14,7 @@ * * @internal */ -class InternalLogger implements LoggerInterface +final class InternalLogger implements LoggerInterface { /** * @param string[] $exceptionsToIgnore @@ -114,7 +114,7 @@ public static function format(string|Stringable $message, array $context = []): /** * @param mixed[] $context */ - private function shouldLog(int $currentLevel, array $context): bool + public function shouldLog(int $currentLevel, array $context): bool { return $currentLevel >= $this->globalLevel && !$this->hasAnythingToIgnore($context); } diff --git a/src/Override/ArrayDataSource.php b/src/Override/ArrayDataSource.php index edcb831..40d4a38 100644 --- a/src/Override/ArrayDataSource.php +++ b/src/Override/ArrayDataSource.php @@ -4,7 +4,7 @@ namespace ConfigCat\Override; -use ConfigCat\Attributes\SettingAttributes; +use ConfigCat\ConfigJson\Setting; /** * Describes an array override data source. @@ -27,9 +27,7 @@ public function getOverrides(): array { $result = []; foreach ($this->overrides as $key => $value) { - $result[$key] = [ - SettingAttributes::VALUE => $value, - ]; + $result[$key] = Setting::fromValue($value); } return $result; diff --git a/src/Override/LocalFileDataSource.php b/src/Override/LocalFileDataSource.php index 311452d..0d2896d 100644 --- a/src/Override/LocalFileDataSource.php +++ b/src/Override/LocalFileDataSource.php @@ -4,9 +4,10 @@ namespace ConfigCat\Override; -use ConfigCat\Attributes\Config; -use ConfigCat\Attributes\SettingAttributes; +use ConfigCat\ConfigJson\Config; +use ConfigCat\ConfigJson\Setting; use InvalidArgumentException; +use UnexpectedValueException; /** * Describes a local file override data source. @@ -34,8 +35,8 @@ public function getOverrides(): array { $content = file_get_contents($this->filePath); if (false === $content) { - $this->logger->error("Cannot find the local config file '".$this->filePath."'. ' . - 'This is a path that your application provided to the ConfigCat SDK by passing it to the `FlagOverrides.LocalFile()` method. ' . + $this->logger->error("Cannot find the local config file '".$this->filePath."'. ' . + 'This is a path that your application provided to the ConfigCat SDK by passing it to the `FlagOverrides.LocalFile()` method. ' . 'Read more: https://configcat.com/docs/sdk-reference/php/#json-file", [ 'event_id' => 1300, ]); @@ -44,26 +45,37 @@ public function getOverrides(): array } $json = json_decode($content, true); + if (JSON_ERROR_NONE !== json_last_error()) { + $ex = new UnexpectedValueException('JSON error: '.json_last_error_msg()); + } elseif (is_array($json)) { + if (!isset($json['flags'])) { + Config::fixupSaltAndSegments($json); - if (null == $json) { - $this->logger->error("Failed to decode JSON from the local config file '".$this->filePath."'. JSON error: ".json_last_error_msg(), [ - 'event_id' => 2302, - ]); + try { + return Setting::ensureMap($json[Config::SETTINGS] ?? []); + } catch (UnexpectedValueException $ex) { + // intentional no-op + } + } elseif (is_array($json['flags'])) { + $result = []; - return []; - } + foreach ($json['flags'] as $key => $value) { + $result[$key] = Setting::fromValue($value); + } - if (isset($json['flags'])) { - $result = []; - foreach ($json['flags'] as $key => $value) { - $result[$key] = [ - SettingAttributes::VALUE => $value, - ]; + return $result; + } else { + $ex = new UnexpectedValueException('Invalid config JSON content: '.$content); } - - return $result; + } else { + $ex = new UnexpectedValueException('Invalid config JSON content: '.$content); } - return $json[Config::ENTRIES]; + $this->logger->error("Failed to decode JSON from the local config file '".$this->filePath."'.", [ + 'event_id' => 2302, + 'exception' => $ex, + ]); + + return []; } } diff --git a/src/RolloutEvaluator.php b/src/RolloutEvaluator.php index 896836f..a387c3b 100644 --- a/src/RolloutEvaluator.php +++ b/src/RolloutEvaluator.php @@ -4,421 +4,1260 @@ namespace ConfigCat; -use ConfigCat\Attributes\PercentageAttributes; -use ConfigCat\Attributes\RolloutAttributes; -use ConfigCat\Attributes\SettingAttributes; +use ConfigCat\ConfigJson\ConditionContainer; +use ConfigCat\ConfigJson\PercentageOption; +use ConfigCat\ConfigJson\PrerequisiteFlagComparator; +use ConfigCat\ConfigJson\PrerequisiteFlagCondition; +use ConfigCat\ConfigJson\Segment; +use ConfigCat\ConfigJson\SegmentComparator; +use ConfigCat\ConfigJson\SegmentCondition; +use ConfigCat\ConfigJson\Setting; +use ConfigCat\ConfigJson\SettingType; +use ConfigCat\ConfigJson\SettingValue; +use ConfigCat\ConfigJson\SettingValueContainer; +use ConfigCat\ConfigJson\TargetingRule; +use ConfigCat\ConfigJson\UserComparator; +use ConfigCat\ConfigJson\UserCondition; use ConfigCat\Log\InternalLogger; -use Exception; -use z4kn4fein\SemVer\SemverException; +use ConfigCat\Log\LogLevel; +use DateTimeInterface; +use LogicException; +use stdClass; +use Throwable; +use UnexpectedValueException; use z4kn4fein\SemVer\Version; /** - * Class RolloutEvaluator. - * * @internal */ -final class RolloutEvaluator +final class EvaluateContext { + public bool $isMissingUserObjectLogged; + public bool $isMissingUserObjectAttributeLogged; + + public ?EvaluateLogBuilder $logBuilder = null; // initialized by RolloutEvaluator.evaluate + + private null|SettingType|stdClass $settingType = null; + /** - * @var string[] + * @var null|list */ - private array $comparatorTexts = [ - '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)', - 'IS ONE OF (Sensitive)', - 'IS NOT ONE OF (Sensitive)', - ]; + private ?array $visitedFlags = null; + + /** + * @param string $key the key of the setting to evaluate + * @param mixed $setting the definition of the setting to evaluate + * @param ?User $user the User Object + * @param array $settings the map of settings + */ + public function __construct( + public readonly string $key, + public readonly mixed $setting, + public readonly ?User $user, + public readonly array $settings + ) { + $this->isMissingUserObjectLogged = $this->isMissingUserObjectAttributeLogged = false; + } + + public static function forPrerequisiteFlag(string $key, mixed $setting, EvaluateContext $dependentFlagContext): EvaluateContext + { + $context = new EvaluateContext($key, $setting, $dependentFlagContext->user, $dependentFlagContext->settings); + $context->visitedFlags = &$dependentFlagContext->getVisitedFlags(); // crucial to use `getVisitedFlags` here to make sure the list is created! + $context->logBuilder = $dependentFlagContext->logBuilder; + + return $context; + } + + public function getSettingType(): SettingType|stdClass + { + return $this->settingType ??= Setting::getType($this->setting); // @phpstan-ignore-line + } + + /** + * @return list + */ + public function &getVisitedFlags(): array + { + $this->visitedFlags ??= []; + + return $this->visitedFlags; + } +} + +/** + * @internal + */ +final class EvaluateResult +{ + /** + * @param array $selectedValue + * @param null|array $matchedTargetingRule + * @param null|array $matchedPercentageOption + */ + public function __construct( + public readonly array $selectedValue, + public readonly ?array $matchedTargetingRule = null, + public readonly ?array $matchedPercentageOption = null + ) {} +} + +/** + * @internal + */ +final class RolloutEvaluator +{ + private const TARGETING_RULE_IGNORED_MESSAGE = 'The current targeting rule is ignored and the evaluation continues with the next rule.'; + private const MISSING_USER_OBJECT_ERROR = 'cannot evaluate, User Object is missing'; + private const MISSING_USER_ATTRIBUTE_ERROR = 'cannot evaluate, the User.%s attribute is missing'; + private const INVALID_USER_ATTRIBUTE_ERROR = 'cannot evaluate, the User.%s attribute is invalid (%s)'; /** - * RolloutEvaluator constructor. - * * @param InternalLogger $logger the logger instance */ public function __construct(private readonly InternalLogger $logger) {} /** - * Evaluates a requested value from the configuration by the specified roll-out rules. + * @param mixed $defaultValue the value to return in case of failure + * @param EvaluateContext $context the context object * - * @param string $key the key of the desired value - * @param array $json the decoded JSON configuration - * @param EvaluationLogCollector $logCollector the evaluation log collector - * @param ?User $user Optional. The user to identify the caller. + * @return EvaluateResult the result of the evaluation * - * @return EvaluationResult the evaluation result + * @throws UnexpectedValueException */ - public function evaluate( - string $key, - array $json, - EvaluationLogCollector $logCollector, - ?User $user = null - ): EvaluationResult { - if (null === $user) { - if (isset($json[SettingAttributes::ROLLOUT_RULES]) - && !empty($json[SettingAttributes::ROLLOUT_RULES]) - || isset($json[SettingAttributes::ROLLOUT_PERCENTAGE_ITEMS]) - && !empty($json[SettingAttributes::ROLLOUT_PERCENTAGE_ITEMS])) { - $this->logger->warning("Cannot evaluate targeting rules and % options for setting '".$key."' (User Object is missing). ". - 'You should pass a User Object to the evaluation methods like `getValue()` in order to make targeting work properly. '. - 'Read more: https://configcat.com/docs/advanced/user-object/', [ - 'event_id' => 3001, - ]); - } - - $result = $json[SettingAttributes::VALUE]; - $variationId = $json[SettingAttributes::VARIATION_ID] ?? ''; - $logCollector->add('Returning '.Utils::getStringRepresentation($result).'.'); - - return new EvaluationResult($result, $variationId, null, null); - } - - $logCollector->add('User object: '.$user); - if (isset($json[SettingAttributes::ROLLOUT_RULES]) && !empty($json[SettingAttributes::ROLLOUT_RULES])) { - foreach ($json[SettingAttributes::ROLLOUT_RULES] as $rule) { - $comparisonAttribute = $rule[RolloutAttributes::COMPARISON_ATTRIBUTE]; - $comparisonValue = $rule[RolloutAttributes::COMPARISON_VALUE]; - $comparator = $rule[RolloutAttributes::COMPARATOR]; - $value = $rule[RolloutAttributes::VALUE]; - $variationId = $rule[RolloutAttributes::VARIATION_ID] ?? ''; - $userValue = $user->getAttribute($comparisonAttribute); - - if (empty($comparisonValue) || (!is_numeric($userValue) && empty($userValue))) { - $logCollector->add($this->logNoMatch( - $comparisonAttribute, - $userValue, - $comparator, - $comparisonValue - )); - - continue; + public function evaluate(mixed $defaultValue, EvaluateContext $context, mixed &$returnValue): EvaluateResult + { + $logBuilder = $context->logBuilder; + + // Building the evaluation log is expensive, so let's not do it if it wouldn't be logged anyway. + if ($this->logger->shouldLog(LogLevel::INFO, [])) { + $context->logBuilder = $logBuilder = new EvaluateLogBuilder(); + + $logBuilder->append("Evaluating '{$context->key}'"); + + if (isset($context->user)) { + $logBuilder->append(" for User '{$context->user}'"); + } + + $logBuilder->increaseIndent(); + } + + try { + Setting::ensure($context->setting); + + $settingType = $context->getSettingType(); + $result = $this->evaluateSetting($context); + $returnValue = SettingValue::get($result->selectedValue[SettingValueContainer::VALUE] ?? null, $settingType); + + return $result; + } catch (Throwable $ex) { + $logBuilder?->resetIndent()->increaseIndent(); + + $returnValue = $defaultValue; + + throw $ex; + } finally { + if (isset($logBuilder)) { + $returnValueFormatted = EvaluateLogBuilder::formatSettingValue($returnValue); + $logBuilder->newLine("Returning '{$returnValueFormatted}'.") + ->decreaseIndent() + ; + $this->logger->info((string) $logBuilder, [ + 'event_id' => 5000, + ]); + } + + if (!isset($ex)) { + $this->checkDefaultValueTypeMismatch( + $returnValue, + $defaultValue, + $settingType // @phpstan-ignore-line + ); + } + } + } + + private function evaluateSetting(EvaluateContext $context): EvaluateResult + { + $targetingRules = TargetingRule::ensureList($context->setting[Setting::TARGETING_RULES] ?? []); + if (!empty($targetingRules) && ($evaluateResult = $this->evaluateTargetingRules($targetingRules, $context))) { + return $evaluateResult; + } + + $percentageOptions = PercentageOption::ensureList($context->setting[Setting::PERCENTAGE_OPTIONS] ?? []); + if (!empty($percentageOptions) && ($evaluateResult = $this->evaluatePercentageOptions($percentageOptions, null, $context))) { + return $evaluateResult; + } + + return new EvaluateResult($context->setting); + } + + /** + * @param list> $targetingRules + */ + private function evaluateTargetingRules(array $targetingRules, EvaluateContext $context): ?EvaluateResult + { + $logBuilder = $context->logBuilder; + + $logBuilder?->newLine('Evaluating targeting rules and applying the first match if any:'); + + foreach ($targetingRules as $targetingRule) { + TargetingRule::ensure($targetingRule); + + $conditions = ConditionContainer::ensureList($targetingRule[TargetingRule::CONDITIONS] ?? []); + + $isMatchOrError = $this->evaluateConditions($conditions, ConditionContainer::conditionAccessor(), $targetingRule, $context->key, $context); + if (true !== $isMatchOrError) { + if (is_string($isMatchOrError)) { + $logBuilder?->increaseIndent() + ->newLine(self::TARGETING_RULE_IGNORED_MESSAGE) + ->decreaseIndent() + ; } - switch ($comparator) { - // IS ONE OF - case 0: - $split = array_filter(Utils::splitTrim($comparisonValue)); - if (in_array($userValue, $split, true)) { - $logCollector->add($this->logMatch( - $comparisonAttribute, - $userValue, - $comparator, - $comparisonValue, - $value - )); - - return new EvaluationResult($value, $variationId, $rule, null); - } - - break; - - // IS NOT ONE OF - case 1: - $split = array_filter(Utils::splitTrim($comparisonValue)); - if (!in_array($userValue, $split, true)) { - $logCollector->add($this->logMatch( - $comparisonAttribute, - $userValue, - $comparator, - $comparisonValue, - $value - )); - - return new EvaluationResult($value, $variationId, $rule, null); - } - - break; - - // CONTAINS - case 2: - if (Utils::strContains($userValue, $comparisonValue)) { - $logCollector->add($this->logMatch( - $comparisonAttribute, - $userValue, - $comparator, - $comparisonValue, - $value - )); - - return new EvaluationResult($value, $variationId, $rule, null); - } - - break; - - // DOES NOT CONTAIN - case 3: - if (!Utils::strContains($userValue, $comparisonValue)) { - $logCollector->add($this->logMatch( - $comparisonAttribute, - $userValue, - $comparator, - $comparisonValue, - $value - )); - - return new EvaluationResult($value, $variationId, $rule, null); - } - - break; - - // IS ONE OF, IS NOT ONE OF (SemVer) - case 4: - case 5: - $split = array_filter(Utils::splitTrim($comparisonValue)); - - try { - $matched = false; - foreach ($split as $semVer) { - $matched = Version::equal($userValue, $semVer) || $matched; - } - - if (($matched && 4 == $comparator) || (!$matched && 5 == $comparator)) { - $logCollector->add($this->logMatch( - $comparisonAttribute, - $userValue, - $comparator, - $comparisonValue, - $value - )); - - return new EvaluationResult($value, $variationId, $rule, null); - } - } catch (SemverException) { - $logCollector->add($this->logMatch( - $comparisonAttribute, - $userValue, - $comparator, - $comparisonValue, - $value - )); - - break; - } - - break; - - // LESS THAN, LESS THAN OR EQUALS TO, GREATER THAN, GREATER THAN OR EQUALS TO (SemVer) - case 6: - case 7: - case 8: - case 9: - try { - if ((6 == $comparator - && Version::lessThan($userValue, $comparisonValue)) - || (7 == $comparator - && Version::lessThanOrEqual($userValue, $comparisonValue)) - || (8 == $comparator - && Version::greaterThan($userValue, $comparisonValue)) - || (9 == $comparator - && Version::greaterThanOrEqual($userValue, $comparisonValue))) { - $logCollector->add($this->logMatch( - $comparisonAttribute, - $userValue, - $comparator, - $comparisonValue, - $value - )); - - return new EvaluationResult($value, $variationId, $rule, null); - } - } catch (SemverException $exception) { - $logCollector->add($this->logFormatError( - $comparisonAttribute, - $userValue, - $comparator, - $comparisonValue, - $exception - )); - - break; - } - - break; - - // LESS THAN, LESS THAN OR EQUALS TO, GREATER THAN, GREATER THAN OR EQUALS TO (Number) - case 10: - case 11: - case 12: - case 13: - case 14: - case 15: - $userDouble = str_replace(',', '.', $userValue); - $comparisonDouble = str_replace(',', '.', $comparisonValue); - if (!is_numeric($userDouble)) { - $logCollector->add($this->logFormatErrorWithMessage( - $comparisonAttribute, - $userValue, - $comparator, - $comparisonValue, - $userDouble.'is not a valid number.' - )); - - break; - } - - if (!is_numeric($comparisonDouble)) { - $logCollector->add($this->logFormatErrorWithMessage( - $comparisonAttribute, - $userValue, - $comparator, - $comparisonValue, - $comparisonDouble.'is not a valid number.' - )); - - break; - } - - $userDoubleValue = floatval($userDouble); - $comparisonDoubleValue = floatval($comparisonDouble); - - if ((10 == $comparator && $userDoubleValue == $comparisonDoubleValue) - || (11 == $comparator && $userDoubleValue != $comparisonDoubleValue) - || (12 == $comparator && $userDoubleValue < $comparisonDoubleValue) - || (13 == $comparator && $userDoubleValue <= $comparisonDoubleValue) - || (14 == $comparator && $userDoubleValue > $comparisonDoubleValue) - || (15 == $comparator && $userDoubleValue >= $comparisonDoubleValue)) { - $logCollector->add($this->logMatch( - $comparisonAttribute, - $userValue, - $comparator, - $comparisonValue, - $value - )); - - return new EvaluationResult($value, $variationId, $rule, null); - } - - break; - - // IS ONE OF (Sensitive) - case 16: - $split = array_filter(Utils::splitTrim($comparisonValue)); - if (in_array(sha1($userValue), $split, true)) { - $logCollector->add($this->logMatch( - $comparisonAttribute, - $userValue, - $comparator, - $comparisonValue, - $value - )); - - return new EvaluationResult($value, $variationId, $rule, null); - } - - break; - - // IS NOT ONE OF (Sensitive) - case 17: - $split = array_filter(Utils::splitTrim($comparisonValue)); - if (!in_array(sha1($userValue), $split, true)) { - $logCollector->add($this->logMatch( - $comparisonAttribute, - $userValue, - $comparator, - $comparisonValue, - $value - )); - - return new EvaluationResult($value, $variationId, $rule, null); - } - - break; + continue; + } + + if (!TargetingRule::hasPercentageOptions($targetingRule)) { + $simpleValue = $targetingRule[TargetingRule::SIMPLE_VALUE]; + + return new EvaluateResult($simpleValue, $targetingRule); + } + + $percentageOptions = $targetingRule[TargetingRule::PERCENTAGE_OPTIONS]; + + $logBuilder?->increaseIndent(); + + $evaluateResult = $this->evaluatePercentageOptions($percentageOptions, $targetingRule, $context); + if ($evaluateResult) { + $logBuilder?->decreaseIndent(); + + return $evaluateResult; + } + + $logBuilder?->newLine(self::TARGETING_RULE_IGNORED_MESSAGE) + ->decreaseIndent() + ; + } + + return null; + } + + /** + * @param list> $percentageOptions + * @param array $matchedTargetingRule + */ + private function evaluatePercentageOptions(array $percentageOptions, ?array $matchedTargetingRule, EvaluateContext $context): ?EvaluateResult + { + $logBuilder = $context->logBuilder; + + if (!isset($context->user)) { + $logBuilder?->newLine('Skipping % options because the User Object is missing.'); + + if (!$context->isMissingUserObjectLogged) { + $this->logUserObjectIsMissing($context->key); + $context->isMissingUserObjectLogged = true; + } + + return null; + } + + $percentageOptionsAttributeName = $context->setting[Setting::PERCENTAGE_OPTIONS_ATTRIBUTE] ?? null; + if (!isset($percentageOptionsAttributeName)) { + $percentageOptionsAttributeName = User::IDENTIFIER_ATTRIBUTE; + $percentageOptionsAttributeValue = $context->user->getIdentifier(); + } elseif (is_string($percentageOptionsAttributeName)) { + $percentageOptionsAttributeValue = $context->user->getAttribute($percentageOptionsAttributeName); + } else { + throw new UnexpectedValueException('Percentage evaluation attribute is invalid.'); + } + + if (!isset($percentageOptionsAttributeValue)) { + $logBuilder?->newLine("Skipping % options because the User.{$percentageOptionsAttributeName} attribute is missing."); + + if (!$context->isMissingUserObjectAttributeLogged) { + $this->logUserObjectAttributeIsMissingPercentage($context->key, $percentageOptionsAttributeName); + $context->isMissingUserObjectAttributeLogged = true; + } + + return null; + } + + $logBuilder?->newLine("Evaluating % options based on the User.{$percentageOptionsAttributeName} attribute:"); + + $sha1 = sha1($context->key.self::userAttributeValueToString($percentageOptionsAttributeValue)); + $hashValue = intval(substr($sha1, 0, 7), 16) % 100; + + $logBuilder?->newLine("- Computing hash in the [0..99] range from User.{$percentageOptionsAttributeName} => {$hashValue} (this value is sticky and consistent across all SDKs)"); + + $bucket = 0; + $optionNumber = 1; + + foreach ($percentageOptions as $percentageOption) { + PercentageOption::ensure($percentageOption); + + $percentage = $percentageOption[PercentageOption::PERCENTAGE] ?? null; + if (!(is_int($percentage) || is_double($percentage)) || $percentage < 0) { + throw new UnexpectedValueException('Percentage is missing or invalid.'); + } + + $bucket += $percentage; + + if ($hashValue >= $bucket) { + ++$optionNumber; + + continue; + } + + if (isset($logBuilder)) { + $percentageOptionValue = SettingValue::get($percentageOption[PercentageOption::VALUE] ?? null, $context->getSettingType(), false); + $percentageOptionValueFormatted = EvaluateLogBuilder::formatSettingValue($percentageOptionValue); + $logBuilder->newLine("- Hash value {$hashValue} selects % option {$optionNumber} ({$percentage}%), '{$percentageOptionValueFormatted}'."); + } + + return new EvaluateResult($percentageOption, $matchedTargetingRule, $percentageOption); + } + + throw new UnexpectedValueException('Sum of percentage option percentages are less than 100.'); + } + + /** + * @param list> $conditions + * @param callable(array, string&): (null|array) $conditionAccessor + * @param array $targetingRule + */ + private function evaluateConditions(array $conditions, callable $conditionAccessor, ?array $targetingRule, string $contextSalt, EvaluateContext $context): bool|string + { + $result = true; + + $logBuilder = $context->logBuilder; + $newLineBeforeThen = false; + + $logBuilder?->newLine('- '); + + $i = 0; + foreach ($conditions as $condition) { + $conditionType = ''; + $condition = ConditionContainer::ensure($conditionAccessor($condition, $conditionType)); + + if (isset($logBuilder)) { + if (0 === $i) { + $logBuilder + ->append('IF ') + ->increaseIndent() + ; + } else { + $logBuilder + ->increaseIndent() + ->newLine('AND ') + ; + } + } + + switch ($conditionType) { + case ConditionContainer::USER_CONDITION: + $result = $this->evaluateUserCondition($condition, $contextSalt, $context); + $newLineBeforeThen = count($conditions) > 1; + + break; + + case ConditionContainer::PREREQUISITE_FLAG_CONDITION: + $result = $this->evaluatePrerequisiteFlagCondition($condition, $context); + $newLineBeforeThen = true; + + break; + + case ConditionContainer::SEGMENT_CONDITION: + $result = $this->evaluateSegmentCondition($condition, $context); + $newLineBeforeThen = !is_string($result) || self::MISSING_USER_OBJECT_ERROR !== $result || count($conditions) > 1; + + break; + + default: + throw new LogicException(); // execution should never get here + } + + $success = true === $result; + + if ($logBuilder) { + if (!isset($targetingRule) || count($conditions) > 1) { + $logBuilder->appendConditionConsequence($success); + } + + $logBuilder->decreaseIndent(); + } + + if (!$success) { + break; + } + + ++$i; + } + + if ($targetingRule) { + $logBuilder?->appendTargetingRuleConsequence($targetingRule, $context->getSettingType(), $result, $newLineBeforeThen); + } + + return $result; + } + + /** + * @param array $condition + */ + private function evaluateUserCondition(array $condition, string $contextSalt, EvaluateContext $context): bool|string + { + $logBuilder = $context->logBuilder; + $logBuilder?->appendUserCondition($condition); + + if (!isset($context->user)) { + if (!$context->isMissingUserObjectLogged) { + $this->logUserObjectIsMissing($context->key); + $context->isMissingUserObjectLogged = true; + } + + return self::MISSING_USER_OBJECT_ERROR; + } + + $userAttributeName = $condition[UserCondition::COMPARISON_ATTRIBUTE] ?? null; + if (!is_string($userAttributeName)) { + throw new UnexpectedValueException('Comparison attribute is missing or invalid.'); + } + $userAttributeValue = $context->user->getAttribute($userAttributeName); + + if (!isset($userAttributeValue) || '' === $userAttributeValue) { + $this->logUserObjectAttributeIsMissingCondition(EvaluateLogBuilder::formatUserCondition($condition), $context->key, $userAttributeName); + + return sprintf(self::MISSING_USER_ATTRIBUTE_ERROR, $userAttributeName); + } + + $comparator = UserComparator::tryFrom($condition[UserCondition::COMPARATOR] ?? null); + + switch ($comparator) { + case UserComparator::TEXT_EQUALS: + case UserComparator::TEXT_NOT_EQUALS: + $text = $this->getUserAttributeValueAsText($userAttributeName, $userAttributeValue, $condition, $context->key); + + return $this->evaluateTextEquals( + $text, + $condition[UserCondition::STRING_COMPARISON_VALUE] ?? null, + UserComparator::TEXT_NOT_EQUALS === $comparator + ); + + case UserComparator::SENSITIVE_TEXT_EQUALS: + case UserComparator::SENSITIVE_TEXT_NOT_EQUALS: + $text = $this->getUserAttributeValueAsText($userAttributeName, $userAttributeValue, $condition, $context->key); + + return $this->evaluateSensitiveTextEquals( + $text, + $condition[UserCondition::STRING_COMPARISON_VALUE] ?? null, + self::ensureConfigJsonSalt($context->setting[Setting::CONFIG_JSON_SALT]), + $contextSalt, + UserComparator::SENSITIVE_TEXT_NOT_EQUALS === $comparator + ); + + case UserComparator::TEXT_IS_ONE_OF: + case UserComparator::TEXT_IS_NOT_ONE_OF: + $text = $this->getUserAttributeValueAsText($userAttributeName, $userAttributeValue, $condition, $context->key); + + return $this->evaluateTextIsOneOf( + $text, + $condition[UserCondition::STRINGLIST_COMPARISON_VALUE] ?? null, + UserComparator::TEXT_IS_NOT_ONE_OF === $comparator + ); + + case UserComparator::SENSITIVE_TEXT_IS_ONE_OF: + case UserComparator::SENSITIVE_TEXT_IS_NOT_ONE_OF: + $text = $this->getUserAttributeValueAsText($userAttributeName, $userAttributeValue, $condition, $context->key); + + return $this->evaluateSensitiveTextIsOneOf( + $text, + $condition[UserCondition::STRINGLIST_COMPARISON_VALUE] ?? null, + self::ensureConfigJsonSalt($context->setting[Setting::CONFIG_JSON_SALT]), + $contextSalt, + UserComparator::SENSITIVE_TEXT_IS_NOT_ONE_OF === $comparator + ); + + case UserComparator::TEXT_STARTS_WITH_ANY_OF: + case UserComparator::TEXT_NOT_STARTS_WITH_ANY_OF: + $text = $this->getUserAttributeValueAsText($userAttributeName, $userAttributeValue, $condition, $context->key); + + return $this->evaluateTextSliceEqualsAnyOf( + $text, + $condition[UserCondition::STRINGLIST_COMPARISON_VALUE] ?? null, + true, + UserComparator::TEXT_NOT_STARTS_WITH_ANY_OF === $comparator + ); + + case UserComparator::SENSITIVE_TEXT_STARTS_WITH_ANY_OF: + case UserComparator::SENSITIVE_TEXT_NOT_STARTS_WITH_ANY_OF: + $text = $this->getUserAttributeValueAsText($userAttributeName, $userAttributeValue, $condition, $context->key); + + return $this->evaluateSensitiveTextSliceEqualsAnyOf( + $text, + $condition[UserCondition::STRINGLIST_COMPARISON_VALUE] ?? null, + self::ensureConfigJsonSalt($context->setting[Setting::CONFIG_JSON_SALT]), + $contextSalt, + true, + UserComparator::SENSITIVE_TEXT_NOT_STARTS_WITH_ANY_OF === $comparator + ); + + case UserComparator::TEXT_ENDS_WITH_ANY_OF: + case UserComparator::TEXT_NOT_ENDS_WITH_ANY_OF: + $text = $this->getUserAttributeValueAsText($userAttributeName, $userAttributeValue, $condition, $context->key); + + return $this->evaluateTextSliceEqualsAnyOf( + $text, + $condition[UserCondition::STRINGLIST_COMPARISON_VALUE] ?? null, + false, + UserComparator::TEXT_NOT_ENDS_WITH_ANY_OF === $comparator + ); + + case UserComparator::SENSITIVE_TEXT_ENDS_WITH_ANY_OF: + case UserComparator::SENSITIVE_TEXT_NOT_ENDS_WITH_ANY_OF: + $text = $this->getUserAttributeValueAsText($userAttributeName, $userAttributeValue, $condition, $context->key); + + return $this->evaluateSensitiveTextSliceEqualsAnyOf( + $text, + $condition[UserCondition::STRINGLIST_COMPARISON_VALUE] ?? null, + self::ensureConfigJsonSalt($context->setting[Setting::CONFIG_JSON_SALT]), + $contextSalt, + false, + UserComparator::SENSITIVE_TEXT_NOT_ENDS_WITH_ANY_OF === $comparator + ); + + case UserComparator::TEXT_CONTAINS_ANY_OF: + case UserComparator::TEXT_NOT_CONTAINS_ANY_OF: + $text = $this->getUserAttributeValueAsText($userAttributeName, $userAttributeValue, $condition, $context->key); + + return $this->evaluateTextContainsAnyOf( + $text, + $condition[UserCondition::STRINGLIST_COMPARISON_VALUE] ?? null, + UserComparator::TEXT_NOT_CONTAINS_ANY_OF === $comparator + ); + + case UserComparator::SEMVER_IS_ONE_OF: + case UserComparator::SEMVER_IS_NOT_ONE_OF: + $versionOrError = $this->getUserAttributeValueAsSemVer($userAttributeName, $userAttributeValue, $condition, $context->key); + + return !is_string($versionOrError) + ? $this->evaluateSemVerIsOneOf( + $versionOrError, + $condition[UserCondition::STRINGLIST_COMPARISON_VALUE] ?? null, + UserComparator::SEMVER_IS_NOT_ONE_OF === $comparator + ) + : $versionOrError; + + case UserComparator::SEMVER_LESS: + case UserComparator::SEMVER_LESS_OR_EQUALS: + case UserComparator::SEMVER_GREATER: + case UserComparator::SEMVER_GREATER_OR_EQUALS: + $versionOrError = $this->getUserAttributeValueAsSemVer($userAttributeName, $userAttributeValue, $condition, $context->key); + + return !is_string($versionOrError) + ? $this->evaluateSemVerRelation( + $versionOrError, + $comparator, // @phpstan-ignore-line + $condition[UserCondition::STRING_COMPARISON_VALUE] ?? null + ) + : $versionOrError; + + case UserComparator::NUMBER_EQUALS: + case UserComparator::NUMBER_NOT_EQUALS: + case UserComparator::NUMBER_LESS: + case UserComparator::NUMBER_LESS_OR_EQUALS: + case UserComparator::NUMBER_GREATER: + case UserComparator::NUMBER_GREATER_OR_EQUALS: + $numberOrError = $this->getUserAttributeValueAsNumber($userAttributeName, $userAttributeValue, $condition, $context->key); + + return !is_string($numberOrError) + ? $this->evaluateNumberRelation( + $numberOrError, + $comparator, // @phpstan-ignore-line + $condition[UserCondition::NUMBER_COMPARISON_VALUE] ?? null + ) + : $numberOrError; + + case UserComparator::DATETIME_BEFORE: + case UserComparator::DATETIME_AFTER: + $numberOrError = $this->getUserAttributeValueAsUnixTimeSeconds($userAttributeName, $userAttributeValue, $condition, $context->key); + + return !is_string($numberOrError) + ? $this->evaluateDateTimeRelation( + $numberOrError, + $condition[UserCondition::NUMBER_COMPARISON_VALUE] ?? null, + UserComparator::DATETIME_BEFORE === $comparator + ) + : $numberOrError; + + case UserComparator::ARRAY_CONTAINS_ANY_OF: + case UserComparator::ARRAY_NOT_CONTAINS_ANY_OF: + $arrayOrError = $this->getUserAttributeValueAsStringArray($userAttributeName, $userAttributeValue, $condition, $context->key); + + return !is_string($arrayOrError) + ? $this->evaluateArrayContainsAnyOf( + $arrayOrError, + $condition[UserCondition::STRINGLIST_COMPARISON_VALUE] ?? null, + UserComparator::ARRAY_NOT_CONTAINS_ANY_OF === $comparator + ) + : $arrayOrError; + + case UserComparator::SENSITIVE_ARRAY_CONTAINS_ANY_OF: + case UserComparator::SENSITIVE_ARRAY_NOT_CONTAINS_ANY_OF: + $arrayOrError = $this->getUserAttributeValueAsStringArray($userAttributeName, $userAttributeValue, $condition, $context->key); + + return !is_string($arrayOrError) + ? $this->evaluateSensitiveArrayContainsAnyOf( + $arrayOrError, + $condition[UserCondition::STRINGLIST_COMPARISON_VALUE] ?? null, + self::ensureConfigJsonSalt($context->setting[Setting::CONFIG_JSON_SALT]), + $contextSalt, + UserComparator::SENSITIVE_ARRAY_NOT_CONTAINS_ANY_OF === $comparator + ) + : $arrayOrError; + + default: + throw new UnexpectedValueException('Comparison operator is missing or invalid.'); + } + } + + private static function evaluateTextEquals(string $text, mixed $comparisonValue, bool $negate): bool + { + self::ensureStringComparisonValue($comparisonValue); + + return ($text === $comparisonValue) !== $negate; + } + + private static function evaluateSensitiveTextEquals(string $text, mixed $comparisonValue, string $configJsonSalt, string $contextSalt, bool $negate): bool + { + self::ensureStringComparisonValue($comparisonValue); + + $hash = self::hashComparisonValue($text, $configJsonSalt, $contextSalt); + + return ($hash === $comparisonValue) !== $negate; + } + + private static function evaluateTextIsOneOf(string $text, mixed $comparisonValues, bool $negate): bool + { + self::ensureComparisonValues($comparisonValues); + + foreach ($comparisonValues as $comparisonValue) { + if ($text === self::ensureStringComparisonValue($comparisonValue)) { + return !$negate; + } + } + + return $negate; + } + + private static function evaluateSensitiveTextIsOneOf(string $text, mixed $comparisonValues, string $configJsonSalt, string $contextSalt, bool $negate): bool + { + self::ensureComparisonValues($comparisonValues); + + $hash = self::hashComparisonValue($text, $configJsonSalt, $contextSalt); + + foreach ($comparisonValues as $comparisonValue) { + if ($hash === self::ensureStringComparisonValue($comparisonValue)) { + return !$negate; + } + } + + return $negate; + } + + private static function evaluateTextSliceEqualsAnyOf(string $text, mixed $comparisonValues, bool $startsWith, bool $negate): bool + { + self::ensureComparisonValues($comparisonValues); + + foreach ($comparisonValues as $comparisonValue) { + $item = self::ensureStringComparisonValue($comparisonValue); + + $success = $startsWith ? str_starts_with($text, $item) : str_ends_with($text, $item); + + if ($success) { + return !$negate; + } + } + + return $negate; + } + + private static function evaluateSensitiveTextSliceEqualsAnyOf(string $text, mixed $comparisonValues, string $configJsonSalt, string $contextSalt, bool $startsWith, bool $negate): bool + { + self::ensureComparisonValues($comparisonValues); + + $textLength = strlen($text); + + foreach ($comparisonValues as $comparisonValue) { + $item = self::ensureStringComparisonValue($comparisonValue); + + $index = strpos($item, '_'); + + if (false === $index + || false === ($sliceLength = filter_var(substr($item, 0, $index), FILTER_VALIDATE_INT)) + || '' === ($hash2 = substr($item, $index + 1))) { + self::ensureStringComparisonValue(null); + + break; // execution should never get here (this is just for keeping phpstan happy) + } + + if ($textLength < $sliceLength) { + continue; + } + + $slice = $startsWith ? substr($text, 0, $sliceLength) : substr($text, $textLength - $sliceLength); + + $hash = self::hashComparisonValue($slice, $configJsonSalt, $contextSalt); + if ($hash === $hash2) { + return !$negate; + } + } + + return $negate; + } + + private static function evaluateTextContainsAnyOf(string $text, mixed $comparisonValues, bool $negate): bool + { + self::ensureComparisonValues($comparisonValues); + + foreach ($comparisonValues as $comparisonValue) { + if (false !== strpos($text, self::ensureStringComparisonValue($comparisonValue))) { + return !$negate; + } + } + + return $negate; + } + + private static function evaluateSemVerIsOneOf(Version $version, mixed $comparisonValues, bool $negate): bool + { + self::ensureComparisonValues($comparisonValues); + + $result = false; + + foreach ($comparisonValues as $comparisonValue) { + self::ensureStringComparisonValue($comparisonValue); + + // NOTE: Previous versions of the evaluation algorithm ignore empty comparison values. + // We keep this behavior for backward compatibility. + if ('' === $comparisonValue) { + continue; + } + + $version2 = Version::parseOrNull(trim($comparisonValue)); + if (!$version2) { + // NOTE: Previous versions of the evaluation algorithm ignored invalid comparison values. + // We keep this behavior for backward compatibility. + return false; + } + + if (!$result && 0 === Version::compare($version, $version2)) { + // NOTE: Previous versions of the evaluation algorithm require that + // none of the comparison values are empty or invalid, that is, we can't stop when finding a match. + // We keep this behavior for backward compatibility. + $result = true; + } + } + + return $result !== $negate; + } + + private static function evaluateSemVerRelation(Version $version, UserComparator $comparator, mixed $comparisonValue): bool + { + self::ensureStringComparisonValue($comparisonValue); + + $version2 = Version::parseOrNull(trim($comparisonValue)); + + if (!$version2) { + return false; + } + + $comparisonResult = Version::compare($version, $version2); + + switch ($comparator) { + case UserComparator::SEMVER_LESS: return $comparisonResult < 0; + + case UserComparator::SEMVER_LESS_OR_EQUALS: return $comparisonResult <= 0; + + case UserComparator::SEMVER_GREATER: return $comparisonResult > 0; + + case UserComparator::SEMVER_GREATER_OR_EQUALS: return $comparisonResult >= 0; + + default: throw new LogicException(); + } + } + + private static function evaluateNumberRelation(float $number, UserComparator $comparator, mixed $comparisonValue): bool + { + $number2 = self::ensureNumberComparisonValue($comparisonValue); + + switch ($comparator) { + case UserComparator::NUMBER_EQUALS: return $number === $number2; + + case UserComparator::NUMBER_NOT_EQUALS: return $number !== $number2; + + case UserComparator::NUMBER_LESS: return $number < $number2; + + case UserComparator::NUMBER_LESS_OR_EQUALS: return $number <= $number2; + + case UserComparator::NUMBER_GREATER: return $number > $number2; + + case UserComparator::NUMBER_GREATER_OR_EQUALS: return $number >= $number2; + + default: throw new LogicException(); + } + } + + private static function evaluateDateTimeRelation(float $number, mixed $comparisonValue, bool $before): bool + { + $number2 = self::ensureNumberComparisonValue($comparisonValue); + + return $before ? $number < $number2 : $number > $number2; + } + + /** + * @param list $array + */ + private static function evaluateArrayContainsAnyOf(array $array, mixed $comparisonValues, bool $negate): bool + { + self::ensureComparisonValues($comparisonValues); + + foreach ($array as $text) { + foreach ($comparisonValues as $comparisonValue) { + if ($text === self::ensureStringComparisonValue($comparisonValue)) { + return !$negate; } - $logCollector->add($this->logNoMatch($comparisonAttribute, $userValue, $comparator, $comparisonValue)); } } - if (isset($json[SettingAttributes::ROLLOUT_PERCENTAGE_ITEMS]) - && !empty($json[SettingAttributes::ROLLOUT_PERCENTAGE_ITEMS])) { - $hashCandidate = $key.$user->getIdentifier(); - $stringHash = substr(sha1($hashCandidate), 0, 7); - $intHash = intval($stringHash, 16); - $scale = $intHash % 100; + return $negate; + } + + /** + * @param list $array + */ + private static function evaluateSensitiveArrayContainsAnyOf(array $array, mixed $comparisonValues, string $configJsonSalt, string $contextSalt, bool $negate): bool + { + self::ensureComparisonValues($comparisonValues); - $bucket = 0; - foreach ($json[SettingAttributes::ROLLOUT_PERCENTAGE_ITEMS] as $rule) { - $bucket += $rule[PercentageAttributes::PERCENTAGE]; - if ($scale < $bucket) { - $result = $rule[PercentageAttributes::VALUE]; - $variationId = $rule[PercentageAttributes::VARIATION_ID]; - $logCollector->add( - 'Evaluating % options. Returning '.Utils::getStringRepresentation($result).'.' - ); + foreach ($array as $text) { + $hash = self::hashComparisonValue($text, $configJsonSalt, $contextSalt); - return new EvaluationResult($result, $variationId, null, $rule); + foreach ($comparisonValues as $comparisonValue) { + if ($hash === self::ensureStringComparisonValue($comparisonValue)) { + return !$negate; } } } - $result = $json[SettingAttributes::VALUE]; - $variationId = $json[SettingAttributes::VARIATION_ID] ?? ''; - $logCollector->add('Returning '.Utils::getStringRepresentation($result).'.'); - - return new EvaluationResult($result, $variationId, null, null); - } - - private function logMatch( - string $comparisonAttribute, - string $userValue, - int $comparator, - string $comparisonValue, - mixed $value - ): string { - return 'Evaluating rule: ['.$comparisonAttribute.':'.$userValue.'] '. - '['.$this->comparatorTexts[$comparator].'] '. - '['.$comparisonValue.'] => match, returning: '.Utils::getStringRepresentation($value).'.'; - } - - private function logNoMatch( - string $comparisonAttribute, - ?string $userValue, - int $comparator, - string $comparisonValue - ): string { - return 'Evaluating rule: ['.$comparisonAttribute.':'.$userValue.'] '. - '['.$this->comparatorTexts[$comparator].'] '. - '['.$comparisonValue.'] => no match.'; - } - - private function logFormatError( - string $comparisonAttribute, - string $userValue, - int $comparator, - string $comparisonValue, - Exception $exception - ): string { - $message = 'Evaluating rule: ['.$comparisonAttribute.':'.$userValue.'] '. - '['.$this->comparatorTexts[$comparator].'] '. - '['.$comparisonValue.'] => SKIP rule. Validation error: '.$exception->getMessage().'.'; - $this->logger->warning($message, ['exception' => $exception]); - - return $message; - } - - private function logFormatErrorWithMessage( - string $comparisonAttribute, - string $userValue, - int $comparator, - string $comparisonValue, - string $message - ): string { - $message = 'Evaluating rule: ['.$comparisonAttribute.':'.$userValue.'] '. - '['.$this->comparatorTexts[$comparator].'] '. - '['.$comparisonValue.'] => SKIP rule. Validation error: '.$message.'.'; - $this->logger->warning($message); - - return $message; + return $negate; + } + + /** + * @param array $condition + */ + private function evaluatePrerequisiteFlagCondition(array $condition, EvaluateContext $context): bool + { + $logBuilder = $context->logBuilder; + $logBuilder?->appendPrerequisiteFlagCondition($condition, $context->settings); + + $prerequisiteFlagKey = $condition[PrerequisiteFlagCondition::PREREQUISITE_FLAG_KEY] ?? null; + if (!is_string($prerequisiteFlagKey)) { + throw new UnexpectedValueException('Prerequisite flag key is missing or invalid.'); + } + + $prerequisiteFlag = $context->settings[$prerequisiteFlagKey] ?? null; + if (!is_array($prerequisiteFlag)) { + throw new UnexpectedValueException('Prerequisite flag is missing or invalid.'); + } + + /** @var SettingType|stdClass $prerequisiteFlagType */ + $prerequisiteFlagType = Setting::getType($prerequisiteFlag); + + $comparisonValue = SettingValue::get($condition[PrerequisiteFlagCondition::COMPARISON_VALUE] ?? null, $prerequisiteFlagType, false); + if (!isset($comparisonValue) && !($prerequisiteFlagType instanceof stdClass)) { + $comparisonValue = SettingValue::infer($condition[PrerequisiteFlagCondition::COMPARISON_VALUE] ?? null); + $comparisonValueFormatted = EvaluateLogBuilder::formatSettingValue($comparisonValue); + + throw new UnexpectedValueException("Type mismatch between comparison value '{$comparisonValueFormatted}' and prerequisite flag '{$prerequisiteFlagKey}'."); + } + + $visitedFlags = &$context->getVisitedFlags(); + array_push($visitedFlags, $context->key); + if (in_array($prerequisiteFlagKey, $visitedFlags, true)) { + array_push($visitedFlags, $prerequisiteFlagKey); + $dependencyCycle = Utils::formatStringList($visitedFlags, 0, null, ' -> '); + + throw new UnexpectedValueException("Circular dependency detected between the following depending flags: {$dependencyCycle}."); + } + + $prerequisiteFlagContext = EvaluateContext::forPrerequisiteFlag($prerequisiteFlagKey, $prerequisiteFlag, $context); + + $logBuilder?->newLine('(') + ->increaseIndent() + ->newLine("Evaluating prerequisite flag '{$prerequisiteFlagKey}':") + ; + + $prerequisiteFlagEvaluateResult = $this->evaluateSetting($prerequisiteFlagContext); + + array_pop($visitedFlags); + + $prerequisiteFlagValue = SettingValue::get( + $prerequisiteFlagEvaluateResult->selectedValue[SettingValueContainer::VALUE] ?? null, + $prerequisiteFlagType + ); + + $comparator = PrerequisiteFlagComparator::tryFrom($condition[PrerequisiteFlagCondition::COMPARATOR] ?? null); + + switch ($comparator) { + case PrerequisiteFlagComparator::EQUALS: + $result = $prerequisiteFlagValue === $comparisonValue; + + break; + + case PrerequisiteFlagComparator::NOT_EQUALS: + $result = $prerequisiteFlagValue !== $comparisonValue; + + break; + + default: + throw new UnexpectedValueException('Comparison operator is missing or invalid.'); + } + + if ($logBuilder) { + $prerequisiteFlagValueFormatted = EvaluateLogBuilder::formatSettingValue($prerequisiteFlagValue); + $logBuilder->newLine("Prerequisite flag evaluation result: '{$prerequisiteFlagValueFormatted}'.") + ->newLine('Condition (') + ->appendPrerequisiteFlagCondition($condition, $context->settings) + ->append(') evaluates to ')->appendConditionResult($result)->append('.') + ->decreaseIndent() + ->newLine(')') + ; + } + + return $result; + } + + /** + * @param array $condition + */ + private function evaluateSegmentCondition(array $condition, EvaluateContext $context): bool|string + { + $segments = $context->setting[Setting::CONFIG_SEGMENTS]; + + $logBuilder = $context->logBuilder; + $logBuilder?->appendSegmentCondition($condition, $segments); + + if (!$context->user) { + if (!$context->isMissingUserObjectLogged) { + $this->logUserObjectIsMissing($context->key); + $context->isMissingUserObjectLogged = true; + } + + return self::MISSING_USER_OBJECT_ERROR; + } + + $segments = Segment::ensureList($segments); + + $segmentIndex = $condition[SegmentCondition::SEGMENT_INDEX] ?? null; + if (!is_int($segmentIndex) || $segmentIndex < 0 || count($segments) <= $segmentIndex) { + throw new UnexpectedValueException('Segment reference is invalid.'); + } + + $segment = Segment::ensure($segments[$segmentIndex]); + + $segmentName = $segment[Segment::NAME] ?? null; + if (!is_string($segmentName) || '' === $segmentName) { + throw new UnexpectedValueException('Segment name is missing.'); + } + + $logBuilder?->newLine('(') + ->increaseIndent() + ->newLine("Evaluating segment '{$segmentName}':") + ; + + $conditions = ConditionContainer::ensureList($segment[Segment::CONDITIONS] ?? []); + + $segmentResult = $this->evaluateConditions($conditions, Segment::conditionAccessor(), null, $segmentName, $context); + $result = $segmentResult; + + if (!is_string($result)) { + $comparator = SegmentComparator::tryFrom($condition[SegmentCondition::COMPARATOR] ?? null); + + switch ($comparator) { + case SegmentComparator::IS_IN: + break; + + case SegmentComparator::IS_NOT_IN: + $result = !$result; + + break; + + default: + throw new UnexpectedValueException('Comparison operator is missing or invalid.'); + } + } + + if ($logBuilder) { + $logBuilder->newLine('Segment evaluation result: '); + + if (!is_string($result)) { + $comparatorText = EvaluateLogBuilder::formatSegmentComparator($segmentResult ? SegmentComparator::IS_IN : SegmentComparator::IS_NOT_IN); + $logBuilder->append("User {$comparatorText}"); + } else { + $logBuilder->append($result); + } + $logBuilder->append('.'); + + $logBuilder->newLine('Condition (')->appendSegmentCondition($condition, $segments)->append(')'); + (!is_string($result) + ? $logBuilder->append(' evaluates to ')->appendConditionResult($result) + : $logBuilder->append(' failed to evaluate')) + ->append('.') + ; + + $logBuilder + ->decreaseIndent() + ->newLine(')') + ; + } + + return $result; + } + + private static function ensureConfigJsonSalt(mixed $value): string + { + return is_string($value) + ? $value + : throw new UnexpectedValueException('Config JSON salt is missing or invalid.'); + } + + /** + * @return list + */ + private static function ensureComparisonValues(mixed $comparisonValues): array + { + return array_is_list($comparisonValues) + ? $comparisonValues + : throw new UnexpectedValueException('Comparison value is missing or invalid.'); + } + + private static function ensureStringComparisonValue(mixed $comparisonValue): string + { + return is_string($comparisonValue) + ? $comparisonValue + : throw new UnexpectedValueException('Comparison value is missing or invalid.'); + } + + private static function ensureNumberComparisonValue(mixed $comparisonValue): float + { + return is_float($comparisonValue) || is_int($comparisonValue) + ? (float) $comparisonValue + : throw new UnexpectedValueException('Comparison value is missing or invalid.'); + } + + private static function hashComparisonValue(string $value, string $configJsonSalt, string $contextSalt): string + { + return hash('sha256', $value.$configJsonSalt.$contextSalt); + } + + private static function userAttributeValueToString(mixed $attributeValue): string + { + if (is_string($attributeValue)) { + return $attributeValue; + } + if (is_double($attributeValue) || is_int($attributeValue)) { + return Utils::numberToString($attributeValue); + } + if ($attributeValue instanceof DateTimeInterface) { + if (is_double($unixTimeSeconds = Utils::dateTimeToUnixSeconds($attributeValue))) { + return Utils::numberToString($unixTimeSeconds); + } + } elseif (Utils::isStringList($attributeValue) + && ($stringArrayJson = json_encode($attributeValue, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES))) { + return $stringArrayJson; + } + + return Utils::getStringRepresentation($attributeValue); + } + + /** + * @param array $condition + */ + private function getUserAttributeValueAsText(string $attributeName, mixed $attributeValue, array $condition, string $key): string + { + if (is_string($attributeValue)) { + return $attributeValue; + } + + $text = self::userAttributeValueToString($attributeValue); + $this->logUserObjectAttributeIsAutoConverted(EvaluateLogBuilder::formatUserCondition($condition), $key, $attributeName, $text); + + return $text; + } + + /** + * @param array $condition + */ + private function getUserAttributeValueAsSemVer(string $attributeName, mixed $attributeValue, array $condition, string $key): string|Version + { + if (is_string($attributeValue)) { + $version = Version::parseOrNull(trim($attributeValue)); + if ($version) { + return $version; + } + } + + $attributeValueFormatted = Utils::getStringRepresentation($attributeValue); + + return $this->handleInvalidUserAttribute($condition, $key, $attributeName, "'{$attributeValueFormatted}' is not a valid semantic version"); + } + + /** + * @param array $condition + */ + private function getUserAttributeValueAsNumber(string $attributeName, mixed $attributeValue, array $condition, string $key): float|string + { + if (is_double($attributeValue) || is_int($attributeValue)) { + return (float) $attributeValue; + } + if (is_string($attributeValue)) { + $number = Utils::numberFromString(str_replace(',', '.', $attributeValue)); + if (is_double($number)) { + return $number; + } + } + + $attributeValueFormatted = Utils::getStringRepresentation($attributeValue); + + return $this->handleInvalidUserAttribute($condition, $key, $attributeName, "'{$attributeValueFormatted}' is not a valid decimal number"); + } + + /** + * @param array $condition + */ + private function getUserAttributeValueAsUnixTimeSeconds(string $attributeName, mixed $attributeValue, array $condition, string $key): float|string + { + if ($attributeValue instanceof DateTimeInterface) { + $unixTimeSeconds = Utils::dateTimeToUnixSeconds($attributeValue); + if (is_double($unixTimeSeconds)) { + return $unixTimeSeconds; + } + } elseif (is_double($attributeValue) || is_int($attributeValue)) { + return (float) $attributeValue; + } elseif (is_string($attributeValue)) { + $unixTimeSeconds = Utils::numberFromString(str_replace(',', '.', $attributeValue)); + if (is_double($unixTimeSeconds)) { + return $unixTimeSeconds; + } + } + + $attributeValueFormatted = Utils::getStringRepresentation($attributeValue); + + return $this->handleInvalidUserAttribute($condition, $key, $attributeName, "'{$attributeValueFormatted}' is not a valid Unix timestamp (number of seconds elapsed since Unix epoch)"); + } + + /** + * @param array $condition + * + * @return list + */ + private function getUserAttributeValueAsStringArray(string $attributeName, mixed $attributeValue, array $condition, string $key): array|string + { + if (is_array($attributeValue)) { + if (Utils::isStringList($attributeValue)) { + return $attributeValue; + } + } elseif (is_string($attributeValue)) { + $stringArray = json_decode($attributeValue, true); + if (JSON_ERROR_NONE === json_last_error() && Utils::isStringList($stringArray)) { + return $stringArray; + } + } + + $attributeValueFormatted = Utils::getStringRepresentation($attributeValue); + + return $this->handleInvalidUserAttribute($condition, $key, $attributeName, "'{$attributeValueFormatted}' is not a valid string array"); + } + + private function logUserObjectIsMissing(string $key): void + { + $this->logger->warning("Cannot evaluate targeting rules and % options for setting '{$key}' (User Object is missing). ". + 'You should pass a User Object to the evaluation methods like `getValue()` in order to make targeting work properly. '. + 'Read more: https://configcat.com/docs/advanced/user-object/', [ + 'event_id' => 3001, + ]); + } + + private function logUserObjectAttributeIsMissingPercentage(string $key, string $attributeName): void + { + $this->logger->warning("Cannot evaluate % options for setting '{$key}' (the User.{$attributeName} attribute is missing). ". + "You should set the User.{$attributeName} attribute in order to make targeting work properly. ". + 'Read more: https://configcat.com/docs/advanced/user-object/', [ + 'event_id' => 3003, + ]); + } + + private function logUserObjectAttributeIsMissingCondition(string $condition, string $key, string $attributeName): void + { + $this->logger->warning("Cannot evaluate condition ({$condition}) for setting '{$key}' (the User.{$attributeName} attribute is missing). ". + "You should set the User.{$attributeName} attribute in order to make targeting work properly. ". + 'Read more: https://configcat.com/docs/advanced/user-object/', [ + 'event_id' => 3003, + ]); + } + + private function logUserObjectAttributeIsInvalid(string $condition, string $key, string $reason, string $attributeName): void + { + $this->logger->warning("Cannot evaluate condition ({$condition}) for setting '{$key}' ({$reason}). ". + "Please check the User.{$attributeName} attribute and make sure that its value corresponds to the comparison operator.", [ + 'event_id' => 3004, + ]); + } + + private function logUserObjectAttributeIsAutoConverted(string $condition, string $key, string $attributeName, string $attributeValue): void + { + $this->logger->warning("Evaluation of condition ({$condition}) for setting '{$key}' may not produce the expected result ". + "(the User.{$attributeName} attribute is not a string value, thus it was automatically converted to the string value '{$attributeValue}'). ". + 'Please make sure that using a non-string value was intended.', [ + 'event_id' => 3005, + ]); + } + + /** + * @param array $condition + */ + private function handleInvalidUserAttribute(array $condition, string $key, string $attributeName, string $reason): string + { + $this->logUserObjectAttributeIsInvalid(EvaluateLogBuilder::formatUserCondition($condition), $key, $reason, $attributeName); + + return sprintf(self::INVALID_USER_ATTRIBUTE_ERROR, $attributeName, $reason); + } + + private function checkDefaultValueTypeMismatch(mixed $returnValue, mixed $defaultValue, SettingType $settingType): void + { + if (!isset($defaultValue)) { // when default value is null, the type of return value can be of any allowed type + return; + } + if (is_bool($returnValue)) { + if (is_bool($defaultValue)) { + return; + } + } elseif (is_string($returnValue)) { + if (is_string($defaultValue)) { + return; + } + } elseif (is_int($returnValue) || is_float($returnValue)) { + if (is_int($defaultValue) || is_float($defaultValue)) { + return; + } + } + + $settingTypeName = $settingType->name; + $defaultValueType = gettype($defaultValue); + + $this->logger->warning("The type of a setting does not match the type of the specified default value ({$defaultValue}). ". + "Setting's type was {$settingTypeName} but the default value's type was {$defaultValueType}. ". + "Please make sure that using a default value not matching the setting's type was intended.", [ + 'event_id' => 4002, + ]); } } diff --git a/src/SettingsResult.php b/src/SettingsResult.php index f3cc273..c59821d 100644 --- a/src/SettingsResult.php +++ b/src/SettingsResult.php @@ -7,10 +7,10 @@ /** * @internal */ -class SettingsResult +final class SettingsResult { /** - * @param mixed[] $settings + * @param array $settings */ public function __construct(public array $settings, public float $fetchTime, public bool $hasConfigJson) {} } diff --git a/src/User.php b/src/User.php index 715808e..5249b6e 100644 --- a/src/User.php +++ b/src/User.php @@ -9,53 +9,65 @@ */ final class User { - private string $identifier; + public const IDENTIFIER_ATTRIBUTE = 'Identifier'; + public const EMAIL_ATTRIBUTE = 'Email'; + public const COUNTRY_ATTRIBUTE = 'Country'; /** - * @var array + * @internal */ - private array $attributes = []; + public const WELL_KNOWN_ATTRIBUTES = [self::IDENTIFIER_ATTRIBUTE, self::EMAIL_ATTRIBUTE, self::COUNTRY_ATTRIBUTE]; /** * User constructor. * - * @param string $identifier the identifier of the user - * @param string $email Optional. The email of the user. - * @param string $country Optional. The country attribute of the user. - * @param array $custom custom user attributes + * @param string $identifier the unique identifier of the user or session (e.g. email address, primary key, session ID, etc.) + * @param ?string $email email address of the user + * @param ?string $country country of the user + * @param ?array $custom custom attributes of the user for advanced targeting rule definitions (e.g. user 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, <, >=, 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** (=, <, >=, etc.) + * * accept `int` or `float` values, + * * accept `string` values containing a properly formatted, valid `int` or `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 / AFTER) + * * accept `DateTimeInterface` values, which are automatically converted to a second-based Unix timestamp, + * * accept `int` or `float` values representing a second-based Unix timestamp, + * * accept `string` values containing a properly formatted, valid `int` or `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 `string`, + * * accept `string` values containing a valid JSON string which can be deserialized to an array of `string`, + * * all other values are considered invalid (a warning will be logged and the currently evaluated targeting rule will be skipped). */ public function __construct( - string $identifier, - string $email = '', - string $country = '', - array $custom = [] - ) { - $this->identifier = $this->attributes['Identifier'] = $identifier; - - if (!empty($email)) { - $this->attributes['Email'] = $email; - } - - if (!empty($country)) { - $this->attributes['Country'] = $country; - } - - if (!empty($custom)) { - $this->attributes = array_merge($this->attributes, $custom); - } - } + private string $identifier, + private ?string $email = null, + private ?string $country = null, + private ?array $custom = null + ) {} /** * @return string the string representation of the user */ public function __toString(): string { - $result = json_encode($this->attributes); - if (!$result) { - return ''; - } + $result = json_encode($this->getAllAttributes(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - return $result; + return false !== $result ? $result : ''; } /** @@ -65,7 +77,7 @@ public function __toString(): string */ public function getIdentifier(): string { - return $this->identifier; + return $this->identifier ?? ''; } /** @@ -73,10 +85,48 @@ public function getIdentifier(): string * * @param string $key the key of the user attribute * - * @return null|string the user attribute, or null if it doesn't exist + * @return mixed the user attribute, or null if it doesn't exist */ - public function getAttribute(string $key): ?string + public function getAttribute(string $key): mixed { - return array_key_exists($key, $this->attributes) ? $this->attributes[$key] : null; + switch ($key) { + case self::IDENTIFIER_ATTRIBUTE: return $this->getIdentifier(); + + case self::EMAIL_ATTRIBUTE: return $this->email; + + case self::COUNTRY_ATTRIBUTE: return $this->country; + + default: return $this->custom[$key] ?? null; + } + } + + /** + * Gets all user attributes. + * + * @return array + */ + public function getAllAttributes(): array + { + $result = []; + + $result[self::IDENTIFIER_ATTRIBUTE] = $this->getIdentifier(); + + if (isset($this->email)) { + $result[self::EMAIL_ATTRIBUTE] = $this->email; + } + + if (isset($this->country)) { + $result[self::COUNTRY_ATTRIBUTE] = $this->country; + } + + if (isset($this->custom)) { + foreach ($this->custom as $attributeName => $attributeValue) { + if (isset($attributeValue) && !in_array($attributeName, self::WELL_KNOWN_ATTRIBUTES, true)) { + $result[$attributeName] = $attributeValue; + } + } + } + + return $result; } } diff --git a/src/Utils.php b/src/Utils.php index c508854..edc8f0f 100644 --- a/src/Utils.php +++ b/src/Utils.php @@ -4,62 +4,206 @@ namespace ConfigCat; +use DateTimeImmutable; +use DateTimeInterface; +use Throwable; + /** * Contains helper utility operations. * * @internal */ -final class Utils +abstract class Utils { /** - * Determines that a string contains another string. + * Returns the string representation of a value. * - * @param string $haystack the string in we search for the other - * @param string $needle the string we search + * @param mixed $value the value * - * @return bool true if the $haystack contains the $needle + * @return string the result string */ - public static function strContains(string $haystack, string $needle): bool + public static function getStringRepresentation(mixed $value): string + { + if (is_bool($value)) { + return $value ? 'true' : 'false'; + } + + try { + return (string) $value; + } catch (Throwable) { // @phpstan-ignore-line + return str_replace(["\r\n", "\r", "\n"], ' ', var_export($value, true)); + } + } + + public static function numberToString(float|int $number): string + { + if (is_nan($number)) { + return 'NaN'; + } + if (is_infinite($number)) { + return $number > 0 ? 'Infinity' : '-Infinity'; + } + if (!$number) { + return '0'; + } + + $abs = abs($number); + if (1e-6 <= $abs && $abs < 1e21) { + $exp = 0; + } else { + $exp = self::getExponent($abs); + $number /= pow(10, $exp); + } + + // NOTE: number_format can't really deal with 17 decimal places, + // e.g. number_format(0.1, 17, '.', '') results in '0.10000000000000001'. + // So we need to manually calculate the actual number of significant decimals. + $decimals = self::getSignificantDecimals($number); + + $str = number_format($number, $decimals, '.', ''); + if ($exp) { + $str .= ($exp > 0 ? 'e+' : 'e').number_format($exp, 0, '.', ''); + } + + return $str; + } + + public static function numberFromString(string $str): false|float { - return str_contains($haystack, $needle); + $str = trim($str); + + switch ($str) { + case 'Infinity': + case '+Infinity': + return INF; + + case '-Infinity': + return -INF; + + case 'NaN': + return NAN; + + default: + return filter_var($str, FILTER_VALIDATE_FLOAT); + } } /** - * Splits a given string and trims the result items. - * - * @param string $text the text to split and trim - * @param non-empty-string $delimiter the delimiter + * Returns the Unix timestamp in milliseconds. * - * @return string[] the array of split items + * @return float milliseconds since epoch */ - public static function splitTrim(string $text, string $delimiter = ','): array + public static function getUnixMilliseconds(): float + { + return floor(microtime(true) * 1000); + } + + public static function dateTimeToUnixSeconds(DateTimeInterface $dateTime): ?float + { + $timestamp = (float) $dateTime->format('U\.v'); + + // Allow values only between 0001-01-01T00:00:00.000Z and 9999-12-31T23:59:59.999 + return $timestamp < -62135596800 || 253402300800 <= $timestamp ? null : $timestamp; + } + + public static function dateTimeFromUnixSeconds(float $timestamp): ?DateTimeInterface + { + // Allow values only between 0001-01-01T00:00:00.000Z and 9999-12-31T23:59:59.999 + if ($timestamp < -62135596800 || 253402300800 <= $timestamp) { + return null; + } + + $dateTime = DateTimeImmutable::createFromFormat('U\\.v', sprintf('%1.3F', $timestamp)); + if (!$dateTime) { + return null; + } + + return $dateTime; + } + + public static function formatDateTimeISO(DateTimeInterface $dateTime): string + { + $timeOffset = $dateTime->getOffset(); + + return $dateTime->format($timeOffset ? 'Y-m-d\\TH:i:s.uP' : 'Y-m-d\\TH:i:s.u\Z'); + } + + public static function isStringList(mixed $value): bool { - return array_map('trim', explode($delimiter, $text)); + return is_array($value) && !self::array_some($value, function ($value, $key, $i) { + return $key !== $i || !is_string($value); + }); } /** - * Returns the string representation of a value. - * - * @param mixed $value the value - * - * @return string the result string + * @param list $items + * @param null|callable(int): string $getOmittedItemsText */ - public static function getStringRepresentation(mixed $value): string + public static function formatStringList(array $items, int $maxCount = 0, ?callable $getOmittedItemsText = null, string $separator = ', '): string { - if (true === is_bool($value)) { - return $value ? 'true' : 'false'; + $count = count($items); + if (!$count) { + return ''; + } + + $appendix = ''; + + if ($maxCount > 0 && $count > $maxCount) { + $items = array_slice($items, 0, $maxCount); + if ($getOmittedItemsText) { + $appendix = $getOmittedItemsText($count - $maxCount); + } } - return (string) $value; + return "'".join("'".$separator."'", $items)."'".$appendix; } /** - * Returns the Unix timestamp in milliseconds. + * @param mixed[] $array the array to check + * @param callable(mixed, int|string, int, mixed[]): bool $isMatch a function to execute for each element in the array; it should return a truthy value to indicate the element passes the test, and a falsy value otherwise * - * @return float milliseconds since epoch + * @return bool `false` unless `$isMatch` returns a truthy value for an array element, in which case true is immediately returned */ - public static function getUnixMilliseconds(): float + private static function array_some(array $array, callable $isMatch): bool { - return floor(microtime(true) * 1000); + $i = 0; + foreach ($array as $key => $value) { + if ($isMatch($value, $key, $i, $array)) { + return true; + } + ++$i; + } + + return false; + } + + private static function getExponent(float|int $abs): int + { + $exp = log10($abs); + $ceil = ceil($exp); + + return (int) (abs($exp - $ceil) < PHP_FLOAT_EPSILON ? $ceil : floor($exp)); + } + + // Based on: https://stackoverflow.com/a/31888253/8656352 + private static function getSignificantDecimals(float|int $number): int + { + if (!$number) { + return 0; + } + + $number = abs($number); + $exp = min(0, self::getExponent($number)); + + for (; $exp > -17; --$exp) { + $fracr = round($number, -$exp, PHP_ROUND_HALF_UP); + // NOTE: PHP_FLOAT_EPSILON is the same as JavaScript's Number.EPSILON + // (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/EPSILON). + if (abs($number - $fracr) < $number * 10.0 * PHP_FLOAT_EPSILON) { + break; + } + } + + return min(17, -$exp); } } diff --git a/tests/CacheTest.php b/tests/CacheTest.php index cd2774d..ac7219e 100644 --- a/tests/CacheTest.php +++ b/tests/CacheTest.php @@ -14,11 +14,11 @@ class CacheTest extends TestCase { - private const TEST_JSON = '{ "f" : { "first": { "v": false, "p": [], "r": [], "i":"fakeIdFirst" }, "second": { "v": true, "p": [], "r": [], "i":"fakeIdSecond" }}}'; + private const TEST_JSON = '{"f":{"first":{"t":0,"v":{"b":false},"i":"fakeIdFirst"},"second":{"t":0,"v":{"b":true},"i":"fakeIdSecond"}}}'; public function testCachePayload() { - $testJson = '{"p":{"u":"https://cdn-global.configcat.com","r":0},"f":{"testKey":{"v":"testValue","t":1,"p":[],"r":[]}}}'; + $testJson = '{"p":{"u":"https://cdn-global.configcat.com","r":0},"f":{"testKey":{"t":1,"v":{"s":"testValue"}}}}'; $dateTime = new DateTime('2023-06-14T15:27:15.8440000Z'); @@ -76,8 +76,8 @@ public function testCacheKeyGeneration($sdkKey, $cacheKey) public function cacheKeyTestData(): array { return [ - ['test1', '147c5b4c2b2d7c77e1605b1a4309f0ea6684a0c6'], - ['test2', 'c09513b1756de9e4bc48815ec7a142b2441ed4d5'], + ['configcat-sdk-1/TEST_KEY-0123456789012/1234567890123456789012', 'f83ba5d45bceb4bb704410f51b704fb6dfa19942'], + ['configcat-sdk-1/TEST_KEY2-123456789012/1234567890123456789012', 'da7bfd8662209c8ed3f9db96daed4f8d91ba5876'], ]; } } diff --git a/tests/ConfigCatClientTest.php b/tests/ConfigCatClientTest.php index a1207d8..53cbbad 100644 --- a/tests/ConfigCatClientTest.php +++ b/tests/ConfigCatClientTest.php @@ -11,6 +11,7 @@ use ConfigCat\Http\GuzzleFetchClient; use ConfigCat\Log\InternalLogger; use ConfigCat\Log\LogLevel; +use ConfigCat\Tests\Helpers\Utils; use ConfigCat\User; use Exception; use GuzzleHttp\Exception\ConnectException; @@ -25,7 +26,7 @@ class ConfigCatClientTest extends TestCase { - private const TEST_JSON = '{ "f" : { "first": { "v": false, "p": [], "r": [], "i":"fakeIdFirst" }, "second": { "v": true, "p": [], "r": [], "i":"fakeIdSecond" }}}'; + private const TEST_JSON = '{"f":{"first":{"t":0,"v":{"b":false},"i":"fakeIdFirst"},"second":{"t":0,"v":{"b":true},"i":"fakeIdSecond"}}}'; public function testConstructEmptySdkKey() { @@ -38,7 +39,7 @@ public function testConstructEmptySdkKey() */ public function testConstructDefaults() { - $client = new ConfigCatClient('testConstructDefaults'); + $client = new ConfigCatClient('testConstructDefaults-/1234567890123456789012'); $logger = $this->getReflectedValue($client, 'logger'); $this->assertInstanceOf(InternalLogger::class, $logger); @@ -56,7 +57,7 @@ public function testConstructDefaults() public function testConstructLoggerOption() { $logger = Utils::getNullLogger(); - $client = new ConfigCatClient('testConstructLoggerOption', [ + $client = new ConfigCatClient('testConstructLoggerOpt/ion-567890123456789012', [ ClientOptions::LOGGER => $logger, ClientOptions::LOG_LEVEL => LogLevel::ERROR, ClientOptions::EXCEPTIONS_TO_IGNORE => [InvalidArgumentException::class], @@ -78,7 +79,7 @@ public function testConstructLoggerOption() public function testConstructCacheOption() { $cache = new ArrayCache(); - $client = new ConfigCatClient('testConstructCacheOption', [ClientOptions::CACHE => $cache]); + $client = new ConfigCatClient('testConstructCacheOpti/on-4567890123456789012', [ClientOptions::CACHE => $cache]); $propCache = $this->getReflectedValue($client, 'cache'); $this->assertSame($cache, $propCache); } @@ -88,14 +89,57 @@ public function testConstructCacheOption() */ public function testConstructCacheRefreshIntervalOption() { - $client = new ConfigCatClient('testConstructCacheRefreshIntervalOption', [ClientOptions::CACHE_REFRESH_INTERVAL => 20]); + $client = new ConfigCatClient('testConstructCacheRefr/eshIntervalOption-9012', [ClientOptions::CACHE_REFRESH_INTERVAL => 20]); $propInterval = $this->getReflectedValue($client, 'cacheRefreshInterval'); $this->assertSame(20, $propInterval); } + public function provideTestDataForSdkKeyFormat_ShouldBeValidated() + { + return Utils::withDescription([ + ['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], + ], function ($testCase) { + return "sdkKey: {$testCase[0]} | customBaseUrl: {$testCase[1]}"; + }); + } + + /** + * @dataProvider provideTestDataForSdkKeyFormat_ShouldBeValidated + */ + public function testSdkKeyFormatShouldBeValidated(string $sdkKey, bool $customBaseUrl, bool $isValid) + { + $clientOptions = $customBaseUrl + ? [ClientOptions::BASE_URL => 'https://my-configcat-proxy'] + : []; + + if (!$isValid) { + $this->expectException(InvalidArgumentException::class); + } else { + $this->expectNotToPerformAssertions(); + } + + $client = new ConfigCatClient($sdkKey, $clientOptions); + } + public function testGetValueFailedFetch() { - $client = new ConfigCatClient('testGetValueFailedFetch', [ClientOptions::FETCH_CLIENT => GuzzleFetchClient::create([ + $client = new ConfigCatClient('testGetValueFailedFetc/h-34567890123456789012', [ClientOptions::FETCH_CLIENT => GuzzleFetchClient::create([ 'handler' => new MockHandler( [new Response(400)] ), @@ -107,7 +151,7 @@ public function testGetValueFailedFetch() public function testGetAllKeysFailedFetch() { - $client = new ConfigCatClient('testGetAllKeysFailedFetch', [ClientOptions::FETCH_CLIENT => GuzzleFetchClient::create([ + $client = new ConfigCatClient('testGetAllKeysFailedFe/tch-567890123456789012', [ClientOptions::FETCH_CLIENT => GuzzleFetchClient::create([ 'handler' => new MockHandler( [new Response(400)] ), @@ -147,7 +191,7 @@ public function testCacheExpiration() $mockHandler = new MockHandler( [new Response(200, [], self::TEST_JSON)] ); - $client = new ConfigCatClient('testCacheExpiration', [ + $client = new ConfigCatClient('testCacheExpiration-12/1234567890123456789012', [ ClientOptions::CACHE => $cache, ClientOptions::FETCH_CLIENT => GuzzleFetchClient::create([ 'handler' => $mockHandler, @@ -175,7 +219,7 @@ public function testCacheExpiration() public function testGetVariationId() { - $client = new ConfigCatClient('testGetVariationId', [ + $client = new ConfigCatClient('testGetVariationId-012/1234567890123456789012', [ ClientOptions::FETCH_CLIENT => GuzzleFetchClient::create([ 'handler' => new MockHandler([new Response(200, [], self::TEST_JSON)]), ]), @@ -187,7 +231,7 @@ public function testGetVariationId() public function testGetAllVariationIds() { - $client = new ConfigCatClient('testGetAllVariationIds', [ + $client = new ConfigCatClient('testGetAllVariationIds/1234567890123456789012', [ ClientOptions::FETCH_CLIENT => GuzzleFetchClient::create([ 'handler' => new MockHandler([new Response(200, [], self::TEST_JSON)]), ]), @@ -199,7 +243,7 @@ public function testGetAllVariationIds() public function testGetAllVariationIdsEmpty() { - $client = new ConfigCatClient('testGetAllVariationIdsEmpty', [ClientOptions::CUSTOM_HANDLER => new MockHandler([ + $client = new ConfigCatClient('testGetAllVariationIds/Empty-7890123456789012', [ClientOptions::CUSTOM_HANDLER => new MockHandler([ new Response(400), ])]); $value = $client->getAllValueDetails(); @@ -209,7 +253,7 @@ public function testGetAllVariationIdsEmpty() public function testGetKeyAndValue() { - $client = new ConfigCatClient('testGetKeyAndValue', [ + $client = new ConfigCatClient('testGetKeyAndValue-012/1234567890123456789012', [ ClientOptions::FETCH_CLIENT => GuzzleFetchClient::create([ 'handler' => new MockHandler([new Response(200, [], self::TEST_JSON)]), ]), @@ -222,7 +266,7 @@ public function testGetKeyAndValue() public function testGetKeyAndValueNull() { - $client = new ConfigCatClient('testGetKeyAndValueNull', [ + $client = new ConfigCatClient('testGetKeyAndValueNull/1234567890123456789012', [ ClientOptions::FETCH_CLIENT => GuzzleFetchClient::create([ 'handler' => new MockHandler([new Response(200, [], self::TEST_JSON)]), ]), @@ -234,7 +278,7 @@ public function testGetKeyAndValueNull() public function testGetAllValues() { - $client = new ConfigCatClient('testGetAllValues', [ + $client = new ConfigCatClient('testGetAllValues-89012/1234567890123456789012', [ ClientOptions::FETCH_CLIENT => GuzzleFetchClient::create([ 'handler' => new MockHandler([new Response(200, [], self::TEST_JSON)]), ]), @@ -246,7 +290,7 @@ public function testGetAllValues() public function testGetAllValueDetails() { - $client = new ConfigCatClient('testGetAllValueDetails', [ + $client = new ConfigCatClient('testGetAllValueDetails/1234567890123456789012', [ ClientOptions::FETCH_CLIENT => GuzzleFetchClient::create([ 'handler' => new MockHandler([new Response(200, [], self::TEST_JSON)]), ]), @@ -259,7 +303,7 @@ public function testGetAllValueDetails() public function testDefaultUser() { - $client = new ConfigCatClient('testDefaultUser', [ClientOptions::CUSTOM_HANDLER => new MockHandler([ + $client = new ConfigCatClient('testDefaultUser-789012/1234567890123456789012', [ClientOptions::CUSTOM_HANDLER => new MockHandler([ new Response(200, [], Utils::formatConfigWithRules()), ])]); @@ -283,7 +327,7 @@ public function testDefaultUser() public function testInitDefaultUser() { $client = new ConfigCatClient( - 'testInitDefaultUser', + 'testInitDefaultUser-12/1234567890123456789012', [ ClientOptions::CUSTOM_HANDLER => new MockHandler([new Response(200, [], Utils::formatConfigWithRules())]), ClientOptions::DEFAULT_USER => new User('test@test1.com'), @@ -306,7 +350,7 @@ public function testInitDefaultUser() public function testDefaultUserVariationId() { - $client = new ConfigCatClient('testDefaultUserVariationId', [ClientOptions::CUSTOM_HANDLER => new MockHandler([ + $client = new ConfigCatClient('testDefaultUserVariati/onId-67890123456789012', [ClientOptions::CUSTOM_HANDLER => new MockHandler([ new Response(200, [], Utils::formatConfigWithRules()), ])]); @@ -337,7 +381,7 @@ public function testOfflineOnline() ] ); - $client = new ConfigCatClient('testOfflineOnline', [ + $client = new ConfigCatClient('testOfflineOnline-9012/1234567890123456789012', [ ClientOptions::CUSTOM_HANDLER => $handler, ]); @@ -370,7 +414,7 @@ public function testInitOfflineOnline() ] ); - $client = new ConfigCatClient('testInitOfflineOnline', [ + $client = new ConfigCatClient('testInitOfflineOnline-/1234567890123456789012', [ ClientOptions::CUSTOM_HANDLER => $handler, ClientOptions::OFFLINE => true, ]); @@ -391,7 +435,7 @@ public function testInitOfflineOnline() public function testHooks() { - $client = new ConfigCatClient('getTestClientWithError', [ + $client = new ConfigCatClient('getTestClientWithError/1234567890123456789012', [ ClientOptions::CUSTOM_HANDLER => new MockHandler( [ new Response(200, [], self::TEST_JSON), @@ -429,7 +473,7 @@ public function testHooks() public function testEvalDetails() { - $client = new ConfigCatClient('testEvalDetails', [ClientOptions::CUSTOM_HANDLER => new MockHandler([ + $client = new ConfigCatClient('testEvalDetails-789012/1234567890123456789012', [ClientOptions::CUSTOM_HANDLER => new MockHandler([ new Response(200, [], Utils::formatConfigWithRules()), ])]); @@ -437,20 +481,21 @@ public function testEvalDetails() $this->assertEquals('fake1', $details->getValue()); $this->assertEquals('id1', $details->getVariationId()); - $this->assertNull($details->getError()); + $this->assertNull($details->getErrorMessage()); $this->assertEquals('key', $details->getKey()); $this->assertEquals('test@test1.com', $details->getUser()->getIdentifier()); - $this->assertEquals('Identifier', $details->getMatchedEvaluationRule()['a']); - $this->assertEquals('@test1.com', $details->getMatchedEvaluationRule()['c']); - $this->assertEquals(2, $details->getMatchedEvaluationRule()['t']); - $this->assertNull($details->getMatchedEvaluationPercentageRule()); + $condition = $details->getMatchedTargetingRule()['c'][0]['u']; + $this->assertEquals('Identifier', $condition['a']); + $this->assertEquals('@test1.com', $condition['l'][0]); + $this->assertEquals(2, $condition['c']); + $this->assertNull($details->getMatchedPercentageOption()); $this->assertTrue($details->getFetchTimeUnixMilliseconds() > 0); $this->assertFalse($details->isDefaultValue()); } public function testEvalDetailsNonExistentFlag() { - $client = new ConfigCatClient('testEvalDetails', [ClientOptions::CUSTOM_HANDLER => new MockHandler([ + $client = new ConfigCatClient('testEvalDetails-789012/1234567890123456789012', [ClientOptions::CUSTOM_HANDLER => new MockHandler([ new Response(200, [], Utils::formatConfigWithRules()), ])]); @@ -458,17 +503,17 @@ public function testEvalDetailsNonExistentFlag() $this->assertEquals('', $details->getValue()); $this->assertEquals('', $details->getVariationId()); - $this->assertNotNull($details->getError()); + $this->assertNotNull($details->getErrorMessage()); $this->assertEquals('non-existent', $details->getKey()); $this->assertEquals('test@test1.com', $details->getUser()->getIdentifier()); - $this->assertNull($details->getMatchedEvaluationRule()); - $this->assertNull($details->getMatchedEvaluationPercentageRule()); + $this->assertNull($details->getMatchedTargetingRule()); + $this->assertNull($details->getMatchedPercentageOption()); $this->assertTrue($details->isDefaultValue()); } public function testEvalDetailsHook() { - $client = new ConfigCatClient('testEvalDetailsHook', [ClientOptions::CUSTOM_HANDLER => new MockHandler([ + $client = new ConfigCatClient('testEvalDetailsHook-12/1234567890123456789012', [ClientOptions::CUSTOM_HANDLER => new MockHandler([ new Response(200, [], Utils::formatConfigWithRules()), ])]); @@ -476,13 +521,14 @@ public function testEvalDetailsHook() $client->hooks()->addOnFlagEvaluated(function (EvaluationDetails $details) use (&$called) { $this->assertEquals('fake1', $details->getValue()); $this->assertEquals('id1', $details->getVariationId()); - $this->assertNull($details->getError()); + $this->assertNull($details->getErrorMessage()); $this->assertEquals('key', $details->getKey()); $this->assertEquals('test@test1.com', $details->getUser()->getIdentifier()); - $this->assertEquals('Identifier', $details->getMatchedEvaluationRule()['a']); - $this->assertEquals('@test1.com', $details->getMatchedEvaluationRule()['c']); - $this->assertEquals(2, $details->getMatchedEvaluationRule()['t']); - $this->assertNull($details->getMatchedEvaluationPercentageRule()); + $condition = $details->getMatchedTargetingRule()['c'][0]['u']; + $this->assertEquals('Identifier', $condition['a']); + $this->assertEquals('@test1.com', $condition['l'][0]); + $this->assertEquals(2, $condition['c']); + $this->assertNull($details->getMatchedPercentageOption()); $this->assertFalse($details->isDefaultValue()); $this->assertTrue($details->getFetchTimeUnixMilliseconds() > 0); $called = true; @@ -495,7 +541,7 @@ public function testEvalDetailsHook() public function testTimeout() { - $client = new ConfigCatClient('testTimout', [ + $client = new ConfigCatClient('testTimeout-3456789012/1234567890123456789012', [ ClientOptions::CUSTOM_HANDLER => new MockHandler([ new ConnectException('timeout', new Request('GET', 'test')), ]), @@ -507,7 +553,7 @@ public function testTimeout() public function testHttpException() { - $client = new ConfigCatClient('testHttpException', [ + $client = new ConfigCatClient('testHttpException-9012/1234567890123456789012', [ ClientOptions::CUSTOM_HANDLER => new MockHandler([ new RequestException('failed', new Request('GET', 'test')), ]), @@ -519,7 +565,7 @@ public function testHttpException() public function testGeneralException() { - $client = new ConfigCatClient('testGeneralException', [ + $client = new ConfigCatClient('testGeneralException-2/1234567890123456789012', [ ClientOptions::CUSTOM_HANDLER => new MockHandler([ new Exception('failed'), ]), diff --git a/tests/ConfigFetcherTest.php b/tests/ConfigFetcherTest.php index 835fd29..e180752 100644 --- a/tests/ConfigFetcherTest.php +++ b/tests/ConfigFetcherTest.php @@ -4,6 +4,7 @@ use ConfigCat\ClientOptions; use ConfigCat\ConfigFetcher; +use ConfigCat\Tests\Helpers\Utils; use GuzzleHttp\Exception\ConnectException; use GuzzleHttp\Handler\MockHandler; use GuzzleHttp\HandlerStack; diff --git a/tests/ConfigV2EvaluationTests.php b/tests/ConfigV2EvaluationTests.php new file mode 100644 index 0000000..f5c0d08 --- /dev/null +++ b/tests/ConfigV2EvaluationTests.php @@ -0,0 +1,616 @@ +%\"'\\/\t\r\n"], '3'], + ['stringArrayToStringConversionUnicode', ['盲枚眉脛脰脺莽茅猫帽谋艧臒芒垄鈩⑩湏馃榾'], '2'], + ], function ($testCase) { + $customAttributeValue = \ConfigCat\Utils::getStringRepresentation($testCase[1]); + + return "key: {$testCase[0]} | customAttributeValue: {$customAttributeValue}"; + }); + } + + /** + * @dataProvider provideTestDataForComparisonAttributeConversionToCanonicalStringRepresentation + */ + public function testComparisonAttributeConversionToCanonicalStringRepresentation(string $key, mixed $customAttributeValue, string $expectedReturnValue) + { + $clientOptions = [ + ClientOptions::FLAG_OVERRIDES => new FlagOverrides( + OverrideDataSource::localFile(self::TEST_DATA_ROOT_PATH.'/comparison_attribute_conversion.json'), + OverrideBehaviour::LOCAL_ONLY + ), + ]; + + $client = new ConfigCatClient('local-only', $clientOptions); + + $user = new User('12345', null, null, [ + 'Custom1' => $customAttributeValue, + ]); + + $defaultValue = 'default'; + $actualReturnValue = $client->getValue($key, $defaultValue, $user); + + $this->assertSame($expectedReturnValue, $actualReturnValue); + } + + public function provideTestDataForComparisonAttributeTrimming() + { + return Utils::withDescription([ + ['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'], + ], function ($testCase) { + return "key: {$testCase[0]}"; + }); + } + + /** + * @dataProvider provideTestDataForComparisonAttributeTrimming + */ + public function testComparisonAttributeTrimming(string $key, string $expectedReturnValue) + { + $clientOptions = [ + ClientOptions::FLAG_OVERRIDES => new FlagOverrides( + OverrideDataSource::localFile(self::TEST_DATA_ROOT_PATH.'/comparison_attribute_trimming.json'), + OverrideBehaviour::LOCAL_ONLY + ), + ]; + + $client = new ConfigCatClient('local-only', $clientOptions); + + $user = new User(' 12345 ', null, '[" USA "]', [ + 'Version' => ' 1.0.0 ', + 'Number' => ' 3 ', + 'Date' => ' 1705253400 ', + ]); + + $defaultValue = 'default'; + $actualReturnValue = $client->getValue($key, $defaultValue, $user); + + $this->assertSame($expectedReturnValue, $actualReturnValue); + } + + public function provideTestDataForComparisonValueTrimming() + { + return Utils::withDescription([ + ['isoneof', 'no trim'], + ['isnotoneof', 'no trim'], + ['containsanyof', 'no trim'], + ['notcontainsanyof', '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 comparator values trimmed because of backward compatibility + ['semverisoneof', '4 trim'], + ['semverisnotoneof', '5 trim'], + ['semverless', '6 trim'], + ['semverlessequals', '7 trim'], + ['semvergreater', '8 trim'], + ['semvergreaterequals', '9 trim'], + ], function ($testCase) { + return "key: {$testCase[0]}"; + }); + } + + /** + * @dataProvider provideTestDataForComparisonValueTrimming_Test + */ + public function testComparisonValueTrimming(string $key, string $expectedReturnValue) + { + $clientOptions = [ + ClientOptions::FLAG_OVERRIDES => new FlagOverrides( + OverrideDataSource::localFile(self::TEST_DATA_ROOT_PATH.'/comparison_value_trimming.json'), + OverrideBehaviour::LOCAL_ONLY + ), + ]; + + $client = new ConfigCatClient('local-only', $clientOptions); + + $user = new User('12345', null, '["USA"]', [ + 'Version' => '1.0.0', + 'Number' => '3', + 'Date' => '1705253400', + ]); + + $defaultValue = 'default'; + $actualReturnValue = $client->getValue($key, $defaultValue, $user); + + $this->assertSame($expectedReturnValue, $actualReturnValue); + } + + public function testUserObjectAttributeValueConversionTextComparisons() + { + $fakeLogger = new FakeLogger(); + + $clientOptions = [ + ClientOptions::LOG_LEVEL => LogLevel::WARNING, + ClientOptions::LOGGER => $fakeLogger, + ]; + + $client = new ConfigCatClient('configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ', $clientOptions); + + $customAttributeName = 'Custom1'; + $customAttributeValue = 42; + $user = new User('12345', null, null, [ + $customAttributeName => $customAttributeValue, + ]); + + $key = 'boolTextEqualsNumber'; + $evaluationDetails = $client->getValueDetails($key, null, $user); + + $this->assertSame(true, $evaluationDetails->getValue()); + + $this->assertEquals(1, count($fakeLogger->events)); + + $event = $fakeLogger->events[0]; + $message = FakeLogger::formatMessage($event); + $expectedAttributeValueText = '42'; + $this->assertSame("WARNING [3005] Evaluation of condition (User.{$customAttributeName} EQUALS '{$expectedAttributeValueText}') for setting '{$key}' may not produce the expected result (the User.{$customAttributeName} attribute is not a string value, thus it was automatically converted to the string value '{$expectedAttributeValueText}'). Please make sure that using a non-string value was intended.", $message); + } + + public function provideTestDataForUserObjectAttributeValueConversion_NonTextComparisons() + { + return Utils::withDescription([ + // 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', -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', 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', PHP_INT_MIN, '<2.1'], + ['configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw', 'numberWithPercentage', '12345', 'Custom1', PHP_INT_MAX, '>5'], + ['configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw', 'numberWithPercentage', '12345', 'Custom1', -INF, '<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', PHP_FLOAT_MIN, '<2.1'], + ['configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw', 'numberWithPercentage', '12345', 'Custom1', PHP_FLOAT_MAX, '>5'], + ['configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw', 'numberWithPercentage', '12345', 'Custom1', +INF, '>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', '-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', new DateTime('2023-03-31T23:59:59.9990000Z'), false], + ['configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ', 'boolTrueIn202304', '12345', 'Custom1', new DateTime('2023-04-01T01:59:59.9990000+02:00'), false], + ['configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ', 'boolTrueIn202304', '12345', 'Custom1', new DateTime('2023-04-01T00:00:00.0010000Z'), true], + ['configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ', 'boolTrueIn202304', '12345', 'Custom1', new DateTime('2023-04-01T02:00:00.0010000+02:00'), true], + ['configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ', 'boolTrueIn202304', '12345', 'Custom1', new DateTime('2023-04-30T23:59:59.9990000Z'), true], + ['configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ', 'boolTrueIn202304', '12345', 'Custom1', new DateTime('2023-05-01T01:59:59.9990000+02:00'), true], + ['configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ', 'boolTrueIn202304', '12345', 'Custom1', new DateTime('2023-05-01T00:00:00.0010000Z'), false], + ['configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ', 'boolTrueIn202304', '12345', 'Custom1', new DateTime('2023-05-01T02:00:00.0010000+02:00'), false], + ['configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ', 'boolTrueIn202304', '12345', 'Custom1', new DateTimeImmutable('2023-03-31T23:59:59.9990000Z'), false], + ['configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ', 'boolTrueIn202304', '12345', 'Custom1', new DateTimeImmutable('2023-04-01T01:59:59.9990000+02:00'), false], + ['configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ', 'boolTrueIn202304', '12345', 'Custom1', new DateTimeImmutable('2023-04-01T00:00:00.0010000Z'), true], + ['configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ', 'boolTrueIn202304', '12345', 'Custom1', new DateTimeImmutable('2023-04-01T02:00:00.0010000+02:00'), true], + ['configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ', 'boolTrueIn202304', '12345', 'Custom1', new DateTimeImmutable('2023-04-30T23:59:59.9990000Z'), true], + ['configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ', 'boolTrueIn202304', '12345', 'Custom1', new DateTimeImmutable('2023-05-01T01:59:59.9990000+02:00'), true], + ['configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ', 'boolTrueIn202304', '12345', 'Custom1', new DateTimeImmutable('2023-05-01T00:00:00.0010000Z'), false], + ['configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ', 'boolTrueIn202304', '12345', 'Custom1', new DateTimeImmutable('2023-05-01T02:00:00.0010000+02:00'), false], + ['configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ', 'boolTrueIn202304', '12345', 'Custom1', -INF, 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', +INF, false], + ['configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ', 'boolTrueIn202304', '12345', 'Custom1', 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'], + ], function ($testCase) { + $customAttributeValue = \ConfigCat\Utils::getStringRepresentation($testCase[4], true); + + return "sdkKey: {$testCase[0]} | key: {$testCase[1]} | userId: {$testCase[2]} | customAttributeName: {$testCase[3]} | customAttributeValue: {$customAttributeValue}"; + }); + } + + /** + * @dataProvider provideTestDataForUserObjectAttributeValueConversion_NonTextComparisons + */ + public function testUserObjectAttributeValueConversionNonTextComparisons( + string $sdkKey, + string $key, + ?string $userId, + string $customAttributeName, + mixed $customAttributeValue, + mixed $expectedReturnValue + ) { + $client = new ConfigCatClient($sdkKey); + + $user = isset($userId) + ? new User($userId, null, null, [ + $customAttributeName => $customAttributeValue, + ]) + : null; + + $evaluationDetails = $client->getValueDetails($key, null, $user); + + $this->assertSame($expectedReturnValue, $evaluationDetails->getValue()); + } + + public function provideTestDataForPrerequisiteFlagCircularDependency() + { + return Utils::withDescription([ + ['key1', "'key1' -> 'key1'"], + ['key2', "'key2' -> 'key3' -> 'key2'"], + ['key4', "'key4' -> 'key3' -> 'key2' -> 'key3'"], + ], function ($testCase) { + return "key: {$testCase[0]} | dependencyCycle: {$testCase[1]}"; + }); + } + + /** + * @dataProvider provideTestDataForPrerequisiteFlagCircularDependency + */ + public function testPrerequisiteFlagCircularDependency(string $key, string $dependencyCycle) + { + $clientOptions = [ + ClientOptions::FLAG_OVERRIDES => new FlagOverrides( + OverrideDataSource::localFile(self::TEST_DATA_ROOT_PATH.'/test_circulardependency_v6.json'), + OverrideBehaviour::LOCAL_ONLY + ), + ]; + + $client = new ConfigCatClient('local-only', $clientOptions); + + $evaluationDetails = $client->getValueDetails($key, null); + + $this->assertTrue($evaluationDetails->isDefaultValue()); + $this->assertNull($evaluationDetails->getValue()); + $this->assertNotNull($evaluationDetails->getErrorMessage()); + $this->assertNotNull($exception = $evaluationDetails->getErrorException()); + $this->assertStringContainsString('Circular dependency detected', $exception->getMessage()); + $this->assertStringContainsString($dependencyCycle, $exception->getMessage()); + } + + public function provideTestDataForPrerequisiteFlagComparisonValueTypeMismatch() + { + // 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 + return Utils::withDescription([ + ['stringDependsOnBool', 'mainBoolFlag', true, 'Dog'], + ['stringDependsOnBool', 'mainBoolFlag', false, 'Cat'], + ['stringDependsOnBool', 'mainBoolFlag', '1', null], + ['stringDependsOnBool', 'mainBoolFlag', 1, null], + ['stringDependsOnBool', 'mainBoolFlag', 1.0, null], + ['stringDependsOnBool', 'mainBoolFlag', [true], null], + ['stringDependsOnBool', 'mainBoolFlag', null, null], + ['stringDependsOnString', 'mainStringFlag', 'private', 'Dog'], + ['stringDependsOnString', 'mainStringFlag', 'Private', 'Cat'], + ['stringDependsOnString', 'mainStringFlag', true, null], + ['stringDependsOnString', 'mainStringFlag', 1, null], + ['stringDependsOnString', 'mainStringFlag', 1.0, null], + ['stringDependsOnString', 'mainStringFlag', ['private'], null], + ['stringDependsOnString', 'mainStringFlag', null, null], + ['stringDependsOnInt', 'mainIntFlag', 2, 'Dog'], + ['stringDependsOnInt', 'mainIntFlag', 1, 'Cat'], + ['stringDependsOnInt', 'mainIntFlag', '2', null], + ['stringDependsOnInt', 'mainIntFlag', true, null], + ['stringDependsOnInt', 'mainIntFlag', 2.0, null], + ['stringDependsOnInt', 'mainIntFlag', [2], null], + ['stringDependsOnInt', 'mainIntFlag', null, null], + ['stringDependsOnDouble', 'mainDoubleFlag', 0.1, 'Dog'], + ['stringDependsOnDouble', 'mainDoubleFlag', 0.11, 'Cat'], + ['stringDependsOnDouble', 'mainDoubleFlag', '0.1', null], + ['stringDependsOnDouble', 'mainDoubleFlag', true, null], + ['stringDependsOnDouble', 'mainDoubleFlag', 1, null], + ['stringDependsOnDouble', 'mainDoubleFlag', [0.1], null], + ['stringDependsOnDouble', 'mainDoubleFlag', null, null], + ], function ($testCase) { + return "key: {$testCase[0]} | prerequisiteFlagKey: {$testCase[1]} | prerequisiteFlagValue: {$testCase[2]}"; + }); + } + + /** + * @dataProvider provideTestDataForPrerequisiteFlagComparisonValueTypeMismatch + */ + public function testPrerequisiteFlagComparisonValueTypeMismatch(string $key, string $prerequisiteFlagKey, mixed $prerequisiteFlagValue, mixed $expectedReturnValue) + { + $overrideArray = [$prerequisiteFlagKey => $prerequisiteFlagValue]; + + $clientOptions = [ + ClientOptions::FLAG_OVERRIDES => new FlagOverrides( + OverrideDataSource::localArray($overrideArray), + OverrideBehaviour::LOCAL_OVER_REMOTE + ), + ]; + + $client = new ConfigCatClient('configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/JoGwdqJZQ0K2xDy7LnbyOg', $clientOptions); + + $evaluationDetails = $client->getValueDetails($key, null); + + if (isset($expectedReturnValue)) { + $this->assertFalse($evaluationDetails->isDefaultValue()); + $this->assertSame($expectedReturnValue, $evaluationDetails->getValue()); + $this->assertNull($evaluationDetails->getErrorMessage()); + $this->assertNull($evaluationDetails->getErrorException()); + } else { + $this->assertTrue($evaluationDetails->isDefaultValue()); + $this->assertNull($evaluationDetails->getValue()); + $this->assertNotNull($evaluationDetails->getErrorMessage()); + $this->assertNotNull($exception = $evaluationDetails->getErrorException()); + if (!isset($prerequisiteFlagValue)) { + $this->assertStringContainsString('Setting value is null', $exception->getMessage()); + } elseif (!is_bool($prerequisiteFlagValue) && !is_string($prerequisiteFlagValue) && !is_int($prerequisiteFlagValue) && !is_double($prerequisiteFlagValue)) { + $this->assertStringMatchesFormat("Setting value '%s' is of an unsupported type%s", $exception->getMessage()); + } else { + $this->assertStringMatchesFormat("Type mismatch between comparison value '%s' and prerequisite flag '%s'%s", $exception->getMessage()); + } + } + } + + public function provideTestDataForPrerequisiteFlagOverride() + { + // 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 + return Utils::withDescription([ + ['stringDependsOnString', '1', 'john@sensitivecompany.com', null, '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, null], + ['stringDependsOnString', '2', 'john@notsensitivecompany.com', null, '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, null], + ['stringDependsOnInt', '1', 'john@sensitivecompany.com', null, '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, null], + ['stringDependsOnInt', '2', 'john@notsensitivecompany.com', null, '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, null], + ], function ($testCase) { + return "key: {$testCase[0]} | userId: {$testCase[1]} | email: {$testCase[2]} | overrideBehaviour: {$testCase[3]}"; + }); + } + + /** + * @dataProvider provideTestDataForPrerequisiteFlagOverride + */ + public function testPrerequisiteFlagOverride(string $key, string $userId, string $email, ?int $overrideBehaviour, mixed $expectedReturnValue) + { + $clientOptions = [ + // 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') + ClientOptions::FLAG_OVERRIDES => isset($overrideBehaviour) + ? new FlagOverrides( + OverrideDataSource::localFile(self::TEST_DATA_ROOT_PATH.'/test_override_flagdependency_v6.json'), + $overrideBehaviour + ) + : null, + ]; + + $client = new ConfigCatClient('configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/JoGwdqJZQ0K2xDy7LnbyOg', $clientOptions); + + $user = new User($userId, $email); + + $evaluationDetails = $client->getValueDetails($key, null, $user); + + if (isset($expectedReturnValue)) { + $this->assertFalse($evaluationDetails->isDefaultValue()); + $this->assertSame($expectedReturnValue, $evaluationDetails->getValue()); + $this->assertNull($evaluationDetails->getErrorMessage()); + $this->assertNull($evaluationDetails->getErrorException()); + } else { + $this->assertTrue($evaluationDetails->isDefaultValue()); + $this->assertNull($evaluationDetails->getValue()); + $this->assertNotNull($evaluationDetails->getErrorMessage()); + } + } + + public function provideTestDataForConfigSaltAndSegmentsOverride() + { + // 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 + return Utils::withDescription([ + ['developerAndBetaUserSegment', '1', 'john@example.com', null, 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', null, 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, null], + ], function ($testCase) { + return "key: {$testCase[0]} | userId: {$testCase[1]} | email: {$testCase[2]} | overrideBehaviour: {$testCase[3]}"; + }); + } + + /** + * @dataProvider provideTestDataForConfigSaltAndSegmentsOverride + */ + public function testConfigSaltAndSegmentsOverride(string $key, string $userId, string $email, ?int $overrideBehaviour, mixed $expectedReturnValue) + { + $clientOptions = [ + // 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'] + ClientOptions::FLAG_OVERRIDES => isset($overrideBehaviour) + ? new FlagOverrides( + OverrideDataSource::localFile(self::TEST_DATA_ROOT_PATH.'/test_override_segments_v6.json'), + $overrideBehaviour + ) + : null, + ]; + + $client = new ConfigCatClient('configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/h99HYXWWNE2bH8eWyLAVMA', $clientOptions); + + $user = new User($userId, $email); + + $evaluationDetails = $client->getValueDetails($key, null, $user); + + if (isset($expectedReturnValue)) { + $this->assertFalse($evaluationDetails->isDefaultValue()); + $this->assertSame($expectedReturnValue, $evaluationDetails->getValue()); + $this->assertNull($evaluationDetails->getErrorMessage()); + $this->assertNull($evaluationDetails->getErrorException()); + } else { + $this->assertTrue($evaluationDetails->isDefaultValue()); + $this->assertNull($evaluationDetails->getValue()); + $this->assertNotNull($evaluationDetails->getErrorMessage()); + } + } + + public function provideTestDataForEvaluationDetailsMatchedEvaluationRuleAndPercantageOption() + { + // 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 + return Utils::withDescription([ + ['configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw', 'stringMatchedTargetingRuleAndOrPercentageOption', null, null, null, 'Cat', false, false], + ['configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw', 'stringMatchedTargetingRuleAndOrPercentageOption', '12345', null, null, 'Cat', false, false], + ['configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw', 'stringMatchedTargetingRuleAndOrPercentageOption', '12345', 'a@example.com', null, 'Dog', true, false], + ['configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw', 'stringMatchedTargetingRuleAndOrPercentageOption', '12345', 'a@configcat.com', null, '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', null, '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], + ], function ($testCase) { + return "sdkKey: {$testCase[0]} | key: {$testCase[1]} | userId: {$testCase[2]} | email: {$testCase[3]} | percentageBase: {$testCase[4]}"; + }); + } + + /** + * @dataProvider provideTestDataForEvaluationDetailsMatchedEvaluationRuleAndPercantageOption + */ + public function testEvaluationDetailsMatchedEvaluationRuleAndPercantageOption( + string $sdkKey, + string $key, + ?string $userId, + ?string $email, + ?string $percentageBase, + string $expectedReturnValue, + bool $expectedIsExpectedMatchedTargetingRuleSet, + bool $expectedIsExpectedMatchedPercentageOptionSet + ) { + $client = new ConfigCatClient($sdkKey); + + $user = isset($userId) + ? new User($userId, $email, null, [ + 'PercentageBase' => $percentageBase, + ]) + : null; + + $evaluationDetails = $client->getValueDetails($key, null, $user); + + $this->assertSame($expectedReturnValue, $evaluationDetails->getValue()); + $this->assertSame($expectedIsExpectedMatchedTargetingRuleSet, null != $evaluationDetails->getMatchedTargetingRule()); + $this->assertSame($expectedIsExpectedMatchedPercentageOptionSet, null != $evaluationDetails->getMatchedPercentageOption()); + } +} diff --git a/tests/DataGovernanceTest.php b/tests/DataGovernanceTest.php index f6b3635..daf88f2 100644 --- a/tests/DataGovernanceTest.php +++ b/tests/DataGovernanceTest.php @@ -4,6 +4,8 @@ use ConfigCat\ClientOptions; use ConfigCat\ConfigFetcher; +use ConfigCat\ConfigJson\Config; +use ConfigCat\Tests\Helpers\Utils; use GuzzleHttp\Handler\MockHandler; use GuzzleHttp\HandlerStack; use GuzzleHttp\Middleware; @@ -12,7 +14,7 @@ class DataGovernanceTest extends TestCase { - const JSON_TEMPLATE = '{ "p": { "u": "%s", "r": %d }, "f": {} }'; + const JSON_TEMPLATE = '{"p":{"u":"%s","r":%d}}'; const CUSTOM_CDN_URL = 'https://custom-cdn.configcat.com'; public function testShouldStayOnServer() @@ -33,7 +35,7 @@ public function testShouldStayOnServer() // Assert $this->assertEquals(1, count($requests)); $this->assertStringContainsString($requests[0]['request']->getUri()->getHost(), ConfigFetcher::GLOBAL_URL); - $this->assertEquals(json_decode($body, true), $response->getConfigEntry()->getConfig()); + $this->assertEquals(Config::deserialize($body), $response->getConfigEntry()->getConfig()); } public function testShouldStayOnSameUrlWithRedirect() @@ -54,7 +56,7 @@ public function testShouldStayOnSameUrlWithRedirect() // Assert $this->assertEquals(1, count($requests)); $this->assertStringContainsString($requests[0]['request']->getUri()->getHost(), ConfigFetcher::GLOBAL_URL); - $this->assertEquals(json_decode($body, true), $response->getConfigEntry()->getConfig()); + $this->assertEquals(Config::deserialize($body), $response->getConfigEntry()->getConfig()); } public function testShouldStayOnSameUrlEvenWhenForced() @@ -75,7 +77,7 @@ public function testShouldStayOnSameUrlEvenWhenForced() // Assert $this->assertEquals(1, count($requests)); $this->assertStringContainsString($requests[0]['request']->getUri()->getHost(), ConfigFetcher::GLOBAL_URL); - $this->assertEquals(json_decode($body, true), $response->getConfigEntry()->getConfig()); + $this->assertEquals(Config::deserialize($body), $response->getConfigEntry()->getConfig()); } public function testShouldRedirectToAnotherServer() @@ -99,7 +101,7 @@ public function testShouldRedirectToAnotherServer() $this->assertEquals(2, count($requests)); $this->assertStringContainsString($requests[0]['request']->getUri()->getHost(), ConfigFetcher::GLOBAL_URL); $this->assertStringContainsString($requests[1]['request']->getUri()->getHost(), ConfigFetcher::EU_ONLY_URL); - $this->assertEquals(json_decode($secondBody, true), $response->getConfigEntry()->getConfig()); + $this->assertEquals(Config::deserialize($secondBody), $response->getConfigEntry()->getConfig()); } public function testShouldRedirectToAnotherServerWhenForced() @@ -123,7 +125,7 @@ public function testShouldRedirectToAnotherServerWhenForced() $this->assertEquals(2, count($requests)); $this->assertStringContainsString($requests[0]['request']->getUri()->getHost(), ConfigFetcher::GLOBAL_URL); $this->assertStringContainsString($requests[1]['request']->getUri()->getHost(), ConfigFetcher::EU_ONLY_URL); - $this->assertEquals(json_decode($secondBody, true), $response->getConfigEntry()->getConfig()); + $this->assertEquals(Config::deserialize($secondBody), $response->getConfigEntry()->getConfig()); } public function testShouldBreakRedirectLoop() @@ -149,7 +151,7 @@ public function testShouldBreakRedirectLoop() $this->assertStringContainsString($requests[0]['request']->getUri()->getHost(), ConfigFetcher::GLOBAL_URL); $this->assertStringContainsString($requests[1]['request']->getUri()->getHost(), ConfigFetcher::EU_ONLY_URL); $this->assertStringContainsString($requests[2]['request']->getUri()->getHost(), ConfigFetcher::GLOBAL_URL); - $this->assertEquals(json_decode($firstBody, true), $response->getConfigEntry()->getConfig()); + $this->assertEquals(Config::deserialize($firstBody), $response->getConfigEntry()->getConfig()); } public function testShouldBreakRedirectLoopWhenForced() @@ -175,7 +177,7 @@ public function testShouldBreakRedirectLoopWhenForced() $this->assertStringContainsString($requests[0]['request']->getUri()->getHost(), ConfigFetcher::GLOBAL_URL); $this->assertStringContainsString($requests[1]['request']->getUri()->getHost(), ConfigFetcher::EU_ONLY_URL); $this->assertStringContainsString($requests[2]['request']->getUri()->getHost(), ConfigFetcher::GLOBAL_URL); - $this->assertEquals(json_decode($firstBody, true), $response->getConfigEntry()->getConfig()); + $this->assertEquals(Config::deserialize($firstBody), $response->getConfigEntry()->getConfig()); } public function testShouldRespectCustomUrlWhenNotForced() @@ -197,7 +199,7 @@ public function testShouldRespectCustomUrlWhenNotForced() // Assert $this->assertEquals(1, count($requests)); $this->assertStringContainsString($requests[0]['request']->getUri()->getHost(), self::CUSTOM_CDN_URL); - $this->assertEquals(json_decode($firstBody, true), $response->getConfigEntry()->getConfig()); + $this->assertEquals(Config::deserialize($firstBody), $response->getConfigEntry()->getConfig()); // Act $response = $fetcher->fetch(''); @@ -205,7 +207,7 @@ public function testShouldRespectCustomUrlWhenNotForced() // Assert $this->assertEquals(2, count($requests)); $this->assertStringContainsString($requests[1]['request']->getUri()->getHost(), self::CUSTOM_CDN_URL); - $this->assertEquals(json_decode($firstBody, true), $response->getConfigEntry()->getConfig()); + $this->assertEquals(Config::deserialize($firstBody), $response->getConfigEntry()->getConfig()); } public function testShouldNotRespectCustomUrlWhenForced() @@ -229,7 +231,7 @@ public function testShouldNotRespectCustomUrlWhenForced() $this->assertEquals(2, count($requests)); $this->assertStringContainsString($requests[0]['request']->getUri()->getHost(), self::CUSTOM_CDN_URL); $this->assertStringContainsString($requests[1]['request']->getUri()->getHost(), ConfigFetcher::GLOBAL_URL); - $this->assertEquals(json_decode($secondBody, true), $response->getConfigEntry()->getConfig()); + $this->assertEquals(Config::deserialize($secondBody), $response->getConfigEntry()->getConfig()); } private function getHandlerStack(array $responses, array &$container = []) diff --git a/tests/EvaluationLogTest.php b/tests/EvaluationLogTest.php new file mode 100644 index 0000000..ba046aa --- /dev/null +++ b/tests/EvaluationLogTest.php @@ -0,0 +1,137 @@ + $testCase) { + $expectedLogFileName = $testCase['expectedLog']; + $testName = $testSetName.'['.$i.'] - '.$expectedLogFileName; + $testCaseDataArray[$testName] = [ + $testSetName, + $sdkKey, + $baseUrlOrOverrideFileName, + $testCase['key'], + $testCase['defaultValue'] ?? null, + $testCase['user'] ?? null, + $testCase['returnValue'] ?? null, + $expectedLogFileName, + ]; + } + } + + return $testCaseDataArray; + } + + /** + * @dataProvider provideTestData + */ + public function testEvaluationLog( + string $testSetName, + ?string $sdkKey, + ?string $baseUrlOrOverrideFileName, + string $key, + mixed $defaultValue, + ?array $userObject, + mixed $expectedReturnValue, + string $expectedLogFileName + ) { + if (isset($userObject)) { + $identifier = $userObject[User::IDENTIFIER_ATTRIBUTE]; + $email = $userObject[User::EMAIL_ATTRIBUTE] ?? null; + $country = $userObject[User::COUNTRY_ATTRIBUTE] ?? null; + $custom = null; + foreach ($userObject as $attributeName => $attributeValue) { + if (!in_array($attributeName, User::WELL_KNOWN_ATTRIBUTES, true)) { + $custom ??= []; + $custom[$attributeName] = $attributeValue; + } + } + $user = new User($identifier, $email, $country, $custom); + } else { + $user = null; + } + + $fakeLogger = new FakeLogger(); + + $clientOptions = [ + ClientOptions::LOG_LEVEL => LogLevel::INFO, + ClientOptions::LOGGER => $fakeLogger, + ]; + + if (!(is_string($sdkKey) && '' !== $sdkKey)) { + $sdkKey = 'local-only'; + $clientOptions[ClientOptions::FLAG_OVERRIDES] = new FlagOverrides( + OverrideDataSource::localFile(self::TEST_DATA_ROOT_PATH.'/_overrides/'.$baseUrlOrOverrideFileName), + OverrideBehaviour::LOCAL_ONLY + ); + } elseif (!empty($baseUrlOrOverrideFileName)) { + $clientOptions[ClientOptions::BASE_URL] = $baseUrlOrOverrideFileName; + } + + $client = new ConfigCatClient($sdkKey, $clientOptions); + + $actualReturnValue = $client->getValue($key, $defaultValue, $user); + + $this->assertEquals($expectedReturnValue, $actualReturnValue); + + $expectedLogFilePath = self::TEST_DATA_ROOT_PATH.'/'.$testSetName.'/'.$expectedLogFileName; + $expectedLogText = ''; + foreach (file($expectedLogFilePath) as $line) { + $expectedLogText .= rtrim($line, "\r\n").PHP_EOL; + } + + $actualLogText = ''; + foreach ($fakeLogger->events as $event) { + $actualLogText .= FakeLogger::formatMessage($event).PHP_EOL; + } + + $this->assertEquals($expectedLogText, $actualLogText); + } +} diff --git a/tests/Helpers/FakeLogger.php b/tests/Helpers/FakeLogger.php new file mode 100644 index 0000000..c63e213 --- /dev/null +++ b/tests/Helpers/FakeLogger.php @@ -0,0 +1,98 @@ +events[] = [ + 'level' => $level, + 'message' => $message, + 'context' => $context, + ]; + } + + /** + * @param mixed[] $context + */ + private static function interpolate(string|Stringable $message, array $context = []): string + { + $replace = []; + foreach ($context as $key => $val) { + if (!is_array($val) && (!is_object($val) || method_exists($val, '__toString'))) { + $replace['{'.$key.'}'] = $val; + } + } + + return strtr((string) $message, $replace); + } +} diff --git a/tests/Helpers/Utils.php b/tests/Helpers/Utils.php new file mode 100644 index 0000000..1d9151d --- /dev/null +++ b/tests/Helpers/Utils.php @@ -0,0 +1,100 @@ +> testData + * @param callable(list, int): array> $getDescription + */ + public static function withDescription(array $testData, callable $getDescription) + { + $testDataWithDescription = []; + $i = 0; + foreach ($testData as $testCase) { + $testDataWithDescription[$getDescription($testCase, $i)] = $testCase; + ++$i; + } + + return $testDataWithDescription; + } + + public static function areCompatibleValues(mixed $value1, mixed $value2): bool + { + return is_bool($value1) && is_bool($value2) + || is_string($value1) && is_string($value2) + || (is_int($value1) || is_double($value1)) && (is_int($value2) || is_double($value2)); + } +} diff --git a/tests/LocalSourceTest.php b/tests/LocalSourceTest.php index d1efd03..519a4d6 100644 --- a/tests/LocalSourceTest.php +++ b/tests/LocalSourceTest.php @@ -15,7 +15,7 @@ class LocalSourceTest extends TestCase { - const TEST_JSON_BODY = '{ "f" : { "disabled": { "v": false, "p": [], "r": [], "i":"fakeIdFirst" }, "enabled": { "v": true, "p": [], "r": [], "i":"fakeIdSecond" }}}'; + const TEST_JSON_BODY = '{"f":{"disabled":{"t":0,"v":{"b":false},"i":"fakeIdFirst"},"enabled":{"t":0,"v":{"b":true},"i":"fakeIdSecond"}}}'; public function testWithNonExistingFile() { @@ -36,7 +36,7 @@ public function testWithInvalidBehavior() public function testWithFile() { $client = new ConfigCatClient('testWithFile', [ - ClientOptions::FLAG_OVERRIDES => new FlagOverrides(OverrideDataSource::localFile('tests/test.json'), OverrideBehaviour::LOCAL_ONLY), + ClientOptions::FLAG_OVERRIDES => new FlagOverrides(OverrideDataSource::localFile('tests/data/test.json'), OverrideBehaviour::LOCAL_ONLY), ]); $this->assertTrue($client->getValue('enabledFeature', false)); @@ -49,7 +49,7 @@ public function testWithFile() public function testWithFileRules() { $client = new ConfigCatClient('testWithFile_Rules', [ - ClientOptions::FLAG_OVERRIDES => new FlagOverrides(OverrideDataSource::localFile('tests/test-rules.json'), OverrideBehaviour::LOCAL_ONLY), + ClientOptions::FLAG_OVERRIDES => new FlagOverrides(OverrideDataSource::localFile('tests/data/test-rules.json'), OverrideBehaviour::LOCAL_ONLY), ]); // without user @@ -65,7 +65,7 @@ public function testWithFileRules() public function testWithSimpleFile() { $client = new ConfigCatClient('testWithSimpleFile', [ - ClientOptions::FLAG_OVERRIDES => new FlagOverrides(OverrideDataSource::localFile('tests/test-simple.json'), OverrideBehaviour::LOCAL_ONLY), + ClientOptions::FLAG_OVERRIDES => new FlagOverrides(OverrideDataSource::localFile('tests/data/test-simple.json'), OverrideBehaviour::LOCAL_ONLY), ]); $this->assertTrue($client->getValue('enabledFeature', false)); @@ -96,7 +96,7 @@ public function testWithArraySource() public function testLocalOverRemote() { - $client = new ConfigCatClient('testLocalOverRemote', [ + $client = new ConfigCatClient('testLocalOverRemote-12/1234567890123456789012', [ ClientOptions::FLAG_OVERRIDES => new FlagOverrides(OverrideDataSource::localArray([ 'enabled' => false, 'nonexisting' => true, @@ -112,7 +112,7 @@ public function testLocalOverRemote() public function testRemoteOverLocal() { - $client = new ConfigCatClient('testRemoteOverLocal', [ + $client = new ConfigCatClient('testRemoteOverLocal-12/1234567890123456789012', [ ClientOptions::FLAG_OVERRIDES => new FlagOverrides(OverrideDataSource::localArray([ 'enabled' => false, 'nonexisting' => true, diff --git a/tests/LoggerTest.php b/tests/LoggerTest.php index 693cbe1..6f41cdc 100644 --- a/tests/LoggerTest.php +++ b/tests/LoggerTest.php @@ -521,7 +521,7 @@ public function testClientNoLog() { $mockLogger = $this->getMockBuilder(LoggerInterface::class)->getMock(); - $client = new ConfigCatClient('not-existing', [ + $client = new ConfigCatClient('not-existing-456789012/1234567890123456789012', [ ClientOptions::LOGGER => $mockLogger, ClientOptions::LOG_LEVEL => LogLevel::NO_LOG, ]); diff --git a/tests/OverrideTest.php b/tests/OverrideTest.php new file mode 100644 index 0000000..0308fe2 --- /dev/null +++ b/tests/OverrideTest.php @@ -0,0 +1,188 @@ + $overrideValue]; + + $fakeLogger = new FakeLogger(); + + $clientOptions = [ + ClientOptions::LOG_LEVEL => LogLevel::WARNING, + ClientOptions::LOGGER => $fakeLogger, + ClientOptions::FLAG_OVERRIDES => new FlagOverrides( + OverrideDataSource::localArray($overrideArray), + OverrideBehaviour::LOCAL_ONLY + ), + ]; + + $client = new ConfigCatClient('local-only', $clientOptions); + + $evaluationDetails = $client->getValueDetails($key, $defaultValue); + $warnings = array_filter($fakeLogger->events, function ($event) { + return LogLevel::WARNING === $event['level'] && 4002 === ($event['context']['event_id'] ?? null); + }); + + if (is_bool($overrideValue) || is_string($overrideValue) || is_int($overrideValue) || is_double($overrideValue)) { + $this->assertFalse($evaluationDetails->isDefaultValue()); + $this->assertSame($expectedReturnValue, $evaluationDetails->getValue()); + $this->assertNull($evaluationDetails->getErrorMessage()); + $this->assertNull($evaluationDetails->getErrorException()); + + $this->assertEquals(Utils::areCompatibleValues($overrideValue, $defaultValue) ? 0 : 1, count($warnings)); + } else { + $this->assertTrue($evaluationDetails->isDefaultValue()); + $this->assertSame($expectedReturnValue, $evaluationDetails->getValue()); + $this->assertNotNull($evaluationDetails->getErrorMessage()); + $this->assertNotNull($evaluationDetails->getErrorException()); + + $this->assertEquals(0, count($warnings)); + + $errors = array_filter($fakeLogger->events, function ($event) { + return LogLevel::ERROR === $event['level'] && 1002 === ($event['context']['event_id'] ?? null); + }); + $this->assertEquals(1, count($errors)); + } + } + + public function provideOverrideValueTypeMismatchShouldBeHandledCorrectly_SimplifiedConfig() + { + return Utils::withDescription([ + ['true', false, true], + ['true', '', true], + ['true', 0, true], + ['true', 0.0, true], + ['"text"', false, 'text'], + ['"text"', '', 'text'], + ['"text"', 0, 'text'], + ['"text"', 0.0, 'text'], + ['42', false, 42], + ['42', '', 42], + ['42', 0, 42], + ['42', 0.0, 42], + ['42.0', false, 42.0], + ['42.0', '', 42.0], + ['42.0', 0, 42.0], + ['42.0', 0.0, 42.0], + ['3.14', false, 3.14], + ['3.14', '', 3.14], + ['3.14', 0, 3.14], + ['3.14', 0.0, 3.14], + ['null', false, false], + ['[]', false, false], + ['{}', false, false], + ], function ($testCase) { + return "overrideValueJson: {$testCase[0]} | defaultValue: {$testCase[1]}"; + }); + } + + /** + * @dataProvider provideOverrideValueTypeMismatchShouldBeHandledCorrectly_SimplifiedConfig + */ + public function testOverrideValueTypeMismatchShouldBeHandledCorrectlySimplifiedConfig(string $overrideValueJson, mixed $defaultValue, mixed $expectedReturnValue) + { + $tempFile = tmpfile(); + + try { + $tempFilePath = stream_get_meta_data($tempFile)['uri']; + $key = 'flag'; + $overrideValue = json_decode($overrideValueJson); + fwrite($tempFile, "{ \"flags\": { \"{$key}\": {$overrideValueJson} } }"); + rewind($tempFile); + + $fakeLogger = new FakeLogger(); + + $clientOptions = [ + ClientOptions::LOG_LEVEL => LogLevel::WARNING, + ClientOptions::LOGGER => $fakeLogger, + ClientOptions::FLAG_OVERRIDES => new FlagOverrides( + OverrideDataSource::localFile($tempFilePath), + OverrideBehaviour::LOCAL_ONLY + ), + ]; + + $client = new ConfigCatClient('local-only', $clientOptions); + + $evaluationDetails = $client->getValueDetails($key, $defaultValue); + $warnings = array_filter($fakeLogger->events, function ($event) { + return LogLevel::WARNING === $event['level'] && 4002 === ($event['context']['event_id'] ?? null); + }); + + if (is_bool($overrideValue) || is_string($overrideValue) || is_int($overrideValue) || is_double($overrideValue)) { + $this->assertFalse($evaluationDetails->isDefaultValue()); + $this->assertSame($expectedReturnValue, $evaluationDetails->getValue()); + $this->assertNull($evaluationDetails->getErrorMessage()); + $this->assertNull($evaluationDetails->getErrorException()); + + $this->assertEquals(Utils::areCompatibleValues($overrideValue, $defaultValue) ? 0 : 1, count($warnings)); + } else { + $this->assertTrue($evaluationDetails->isDefaultValue()); + $this->assertSame($expectedReturnValue, $evaluationDetails->getValue()); + $this->assertNotNull($evaluationDetails->getErrorMessage()); + $this->assertNotNull($evaluationDetails->getErrorException()); + + $this->assertEquals(0, count($warnings)); + + $errors = array_filter($fakeLogger->events, function ($event) { + return LogLevel::ERROR === $event['level'] && 1002 === ($event['context']['event_id'] ?? null); + }); + $this->assertEquals(1, count($errors)); + } + } finally { + fclose($tempFile); + } + } +} diff --git a/tests/RolloutIntegrationsTest.php b/tests/RolloutIntegrationsTest.php index 56ed21d..9673ddd 100644 --- a/tests/RolloutIntegrationsTest.php +++ b/tests/RolloutIntegrationsTest.php @@ -5,6 +5,7 @@ use ConfigCat\ClientOptions; use ConfigCat\ConfigCatClient; use ConfigCat\Log\LogLevel; +use ConfigCat\Tests\Helpers\Utils; use ConfigCat\User; use PHPUnit\Framework\TestCase; @@ -22,7 +23,7 @@ class RolloutIntegrationsTest extends TestCase */ public function testRolloutIntegration($file, $sdkKey, $kind) { - $rows = self::readCsv('tests/'.$file); + $rows = self::readCsv('tests/data/'.$file); $settingKeys = array_slice($rows[0], 4); $customKey = $rows[0][3]; $client = new ConfigCatClient($sdkKey, [ @@ -49,8 +50,8 @@ public function testRolloutIntegration($file, $sdkKey, $kind) if ('##null##' !== $testObjects[0]) { $identifier = $testObjects[0]; - $email = ''; - $country = ''; + $email = null; + $country = null; if (!empty($testObjects[1]) && '##null##' !== $testObjects[1]) { $email = $testObjects[1]; @@ -79,17 +80,11 @@ public function testRolloutIntegration($file, $sdkKey, $kind) if (is_bool($actual)) { $actual = $actual ? 'True' : 'False'; - } - - if (is_int($actual)) { + } elseif (is_int($actual)) { $expected = intval($expected); - } - - if (is_double($actual)) { + } elseif (is_double($actual)) { $expected = floatval($expected); - } - - if ($expected !== $actual) { + } elseif ($expected !== $actual) { $errors[] = sprintf('Identifier: %s, SettingKey: %s, UV: %s, Expected: %s, Result: %s', $testObjects[0], $key, $testObjects[3], $expected, $actual); } ++$count; @@ -102,15 +97,77 @@ public function testRolloutIntegration($file, $sdkKey, $kind) public function rolloutTestData(): array { return [ + // *** Config V1 *** + // https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d62463-86ec-8fde-f5b5-1c5c426fc830/244cf8b0-f604-11e8-b543-f23c917f9d8d ['testmatrix.csv', 'PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A', self::valueKind], + // https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d745f1-f315-7daf-d163-5541d3786e6f/244cf8b0-f604-11e8-b543-f23c917f9d8d ['testmatrix_semantic.csv', 'PKDVCLf-Hq-h-kCzMp-L7Q/BAr3KgLTP0ObzKnBTo5nhA', self::valueKind], + // https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d747f0-5986-c2ef-eef3-ec778e32e10a/244cf8b0-f604-11e8-b543-f23c917f9d8d ['testmatrix_number.csv', 'PKDVCLf-Hq-h-kCzMp-L7Q/uGyK3q9_ckmdxRyI7vjwCw', self::valueKind], + // https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d77fa1-a796-85f9-df0c-57c448eb9934/244cf8b0-f604-11e8-b543-f23c917f9d8d ['testmatrix_semantic_2.csv', 'PKDVCLf-Hq-h-kCzMp-L7Q/q6jMCFIp-EmuAfnmZhPY7w', self::valueKind], + // https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d7b724-9285-f4a7-9fcd-00f64f1e83d5/244cf8b0-f604-11e8-b543-f23c917f9d8d ['testmatrix_sensitive.csv', 'PKDVCLf-Hq-h-kCzMp-L7Q/qX3TP2dTj06ZpCCT1h_SPA', self::valueKind], + // https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d9f207-6883-43e5-868c-cbf677af3fe6/244cf8b0-f604-11e8-b543-f23c917f9d8d + ['testmatrix_segments_old.csv', 'PKDVCLf-Hq-h-kCzMp-L7Q/LcYz135LE0qbcacz2mgXnA', self::valueKind], + // https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d774b9-3d05-0027-d5f4-3e76c3dba752/244cf8b0-f604-11e8-b543-f23c917f9d8d ['testmatrix_variationId.csv', 'PKDVCLf-Hq-h-kCzMp-L7Q/nQ5qkhRAUEa6beEyyrVLBA', self::variationKind], + + // *** Config V2 *** + // 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 + ['testmatrix.csv', 'configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/AG6C1ngVb0CvM07un6JisQ', self::valueKind], + // 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 + ['testmatrix_semantic.csv', 'configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/iV8vH2MBakKxkFZylxHmTg', self::valueKind], + // 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 + ['testmatrix_number.csv', 'configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw', self::valueKind], + // 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 + ['testmatrix_semantic_2.csv', 'configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/U8nt3zEhDEO5S2ulubCopA', self::valueKind], + // 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 + ['testmatrix_sensitive.csv', 'configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/-0YmVOUNgEGKkgRF-rU65g', self::valueKind], + // 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 + ['testmatrix_segments_old.csv', 'configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/y_ZB7o-Xb0Swxth-ZlMSeA', self::valueKind], + // 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 + ['testmatrix_variationId.csv', 'configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/spQnkRTIPEWVivZkWM84lQ', self::variationKind], + // 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 + ['testmatrix_and_or.csv', 'configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/ByMO9yZNn02kXcm72lnY1A', self::valueKind], + // 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 + ['testmatrix_comparators_v6.csv', 'configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ', self::valueKind], + // 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 + ['testmatrix_prerequisite_flag.csv', 'configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/JoGwdqJZQ0K2xDy7LnbyOg', self::valueKind], + // 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 + ['testmatrix_segments.csv', 'configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/h99HYXWWNE2bH8eWyLAVMA', self::valueKind], + // 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 + ['testmatrix_unicode.csv', 'configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/Da6w8dBbmUeMUBhh0iEeQQ', self::valueKind], ]; } + public function provideTestDataForSpecialCharactersWorks() + { + return Utils::withDescription([ + ['specialCharacters', '盲枚眉脛脰脺莽茅猫帽谋艧臒芒垄鈩⑩湏馃榾', '盲枚眉脛脰脺莽茅猫帽谋艧臒芒垄鈩⑩湏馃榾'], + ['specialCharactersHashed', '盲枚眉脛脰脺莽茅猫帽谋艧臒芒垄鈩⑩湏馃榾', '盲枚眉脛脰脺莽茅猫帽谋艧臒芒垄鈩⑩湏馃榾'], + ], function ($testCase) { + return "settingKey: {$testCase[0]} | userId: {$testCase[1]}"; + }); + } + + /** + * @dataProvider provideTestDataForSpecialCharactersWorks + */ + public function testSpecialCharactersWorks( + string $settingKey, + string $userId, + string $expectedReturnValue + ) { + $client = new ConfigCatClient('configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/u28_1qNyZ0Wz-ldYHIU7-g'); + + $user = new User($userId); + + $actualReturnValue = $client->getValue($settingKey, null, $user); + + $this->assertSame($expectedReturnValue, $actualReturnValue); + } + private static function readCsv($file): array { $rows = []; diff --git a/tests/UserTest.php b/tests/UserTest.php index 5fe8d6a..cb0211f 100644 --- a/tests/UserTest.php +++ b/tests/UserTest.php @@ -2,6 +2,7 @@ namespace ConfigCat\Tests; +use ConfigCat\Tests\Helpers\Utils; use ConfigCat\User; use PHPUnit\Framework\TestCase; @@ -18,4 +19,145 @@ public function testGetAttributeEmptyKey() $user = new User('id'); $this->assertEquals('', $user->getAttribute('')); } + + public function testCreateUserWithIdAndEmailAndCountryAllAttributesShouldContainsPassedValues() + { + // Arrange + + $user = new User('id', 'id@example.com', 'US'); + + // Act + + $actualAttributes = $user->getAllAttributes(); + + // Assert + + $this->assertIsArray($actualAttributes); + $this->assertEquals(3, count($actualAttributes)); + + $this->assertArrayHasKey(User::IDENTIFIER_ATTRIBUTE, $actualAttributes); + $this->assertSame('id', $actualAttributes[User::IDENTIFIER_ATTRIBUTE]); + $this->assertSame('id', $user->getAttribute(User::IDENTIFIER_ATTRIBUTE)); + $this->assertSame('id', $user->getIdentifier()); + + $this->assertArrayHasKey(User::EMAIL_ATTRIBUTE, $actualAttributes); + $this->assertSame('id@example.com', $actualAttributes[User::EMAIL_ATTRIBUTE]); + $this->assertSame('id@example.com', $user->getAttribute(User::EMAIL_ATTRIBUTE)); + + $this->assertArrayHasKey(User::COUNTRY_ATTRIBUTE, $actualAttributes); + $this->assertSame('US', $actualAttributes[User::COUNTRY_ATTRIBUTE]); + $this->assertSame('US', $user->getAttribute(User::COUNTRY_ATTRIBUTE)); + } + + public function testUseWellKnownAttributesAsCustomPropertiesShouldNotAppendAllAttributes() + { + // Arrange + + $user = new User('id', 'id@example.com', 'US', [ + 'myCustomAttribute' => 'myCustomAttributeValue', + User::IDENTIFIER_ATTRIBUTE => 'myIdentifier', + User::COUNTRY_ATTRIBUTE => 'United States', + User::EMAIL_ATTRIBUTE => 'otherEmail@example.com', + ]); + + // Act + + $actualAttributes = $user->getAllAttributes(); + + // Assert + + $this->assertIsArray($actualAttributes); + $this->assertEquals(4, count($actualAttributes)); + + $this->assertArrayHasKey(User::IDENTIFIER_ATTRIBUTE, $actualAttributes); + $this->assertSame('id', $actualAttributes[User::IDENTIFIER_ATTRIBUTE]); + $this->assertSame('id', $user->getAttribute(User::IDENTIFIER_ATTRIBUTE)); + $this->assertSame('id', $user->getIdentifier()); + + $this->assertArrayHasKey(User::EMAIL_ATTRIBUTE, $actualAttributes); + $this->assertSame('id@example.com', $actualAttributes[User::EMAIL_ATTRIBUTE]); + $this->assertSame('id@example.com', $user->getAttribute(User::EMAIL_ATTRIBUTE)); + + $this->assertArrayHasKey(User::COUNTRY_ATTRIBUTE, $actualAttributes); + $this->assertSame('US', $actualAttributes[User::COUNTRY_ATTRIBUTE]); + $this->assertSame('US', $user->getAttribute(User::COUNTRY_ATTRIBUTE)); + + $this->assertArrayHasKey('myCustomAttribute', $actualAttributes); + $this->assertSame('myCustomAttributeValue', $actualAttributes['myCustomAttribute']); + $this->assertSame('myCustomAttributeValue', $user->getAttribute('myCustomAttribute')); + } + + public function provideTestDataForUseWellKnownAttributesAsCustomPropertiesWithDifferentNames_ShouldAppendAllAttributes(): array + { + return Utils::withDescription([ + ['identifier', 'myId'], + ['IDENTIFIER', 'myId'], + ['email', 'theBoss@example.com'], + ['EMAIL', 'theBoss@example.com'], + ['eMail', 'theBoss@example.com'], + ['country', 'myHome'], + ['COUNTRY', 'myHome'], + ], function ($testCase) { + return "attributeName: {$testCase[0]} | attributeValue: {$testCase[1]}"; + }); + } + + /** + * @dataProvider provideTestDataForUseWellKnownAttributesAsCustomPropertiesWithDifferentNames_ShouldAppendAllAttributes + */ + public function testUseWellKnownAttributesAsCustomPropertiesWithDifferentNamesShouldAppendAllAttributes(string $attributeName, string $attributeValue) + { + // Arrange + + $user = new User('id', 'id@example.com', 'US', [ + $attributeName => $attributeValue, + ]); + + // Act + + $actualAttributes = $user->getAllAttributes(); + + // Assert + + $this->assertIsArray($actualAttributes); + $this->assertEquals(4, count($actualAttributes)); + + $this->assertArrayHasKey($attributeName, $actualAttributes); + $this->assertSame($attributeValue, $actualAttributes[$attributeName]); + $this->assertSame($attributeValue, $user->getAttribute($attributeName)); + } + + public function provideTestDataForCreateUser_ShouldSetIdentifier() + { + return Utils::withDescription([ + [null, ''], + ['', ''], + ['id', 'id'], + ["\t", "\t"], + ["\u{1F60}0", "\u{1F60}0"], + ], function ($testCase) { + return "identifier: {$testCase[0]}"; + }); + } + + /** + * @dataProvider provideTestDataForCreateUser_ShouldSetIdentifier + */ + public function testCreateUserShouldSetIdentifier(string $identifier, string $expectedValue) + { + // Arrange + + $user = new User($identifier); + + // Act + + $actualAttributes = $user->getAllAttributes(); + + // Assert + + $this->assertArrayHasKey(User::IDENTIFIER_ATTRIBUTE, $actualAttributes); + $this->assertSame($expectedValue, $actualAttributes[User::IDENTIFIER_ATTRIBUTE]); + $this->assertSame($expectedValue, $user->getAttribute(User::IDENTIFIER_ATTRIBUTE)); + $this->assertSame($expectedValue, $user->getIdentifier()); + } } diff --git a/tests/Utils.php b/tests/Utils.php deleted file mode 100644 index a35a464..0000000 --- a/tests/Utils.php +++ /dev/null @@ -1,42 +0,0 @@ -assertSame($expectedReturnValue, \ConfigCat\Utils::numberToString($number)); + } +} diff --git a/tests/data/comparison_attribute_conversion.json b/tests/data/comparison_attribute_conversion.json new file mode 100644 index 0000000..5a900ae --- /dev/null +++ b/tests/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/tests/data/comparison_attribute_trimming.json b/tests/data/comparison_attribute_trimming.json new file mode 100644 index 0000000..a42df5f --- /dev/null +++ b/tests/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/tests/data/comparison_value_trimming.json b/tests/data/comparison_value_trimming.json new file mode 100644 index 0000000..db91703 --- /dev/null +++ b/tests/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/tests/data/evaluationlog/1_targeting_rule.json b/tests/data/evaluationlog/1_targeting_rule.json new file mode 100644 index 0000000..596bd2b --- /dev/null +++ b/tests/data/evaluationlog/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/tests/data/evaluationlog/1_targeting_rule/1_rule_matching_targeted_attribute.txt b/tests/data/evaluationlog/1_targeting_rule/1_rule_matching_targeted_attribute.txt new file mode 100644 index 0000000..f05c6f6 --- /dev/null +++ b/tests/data/evaluationlog/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/tests/data/evaluationlog/1_targeting_rule/1_rule_no_targeted_attribute.txt b/tests/data/evaluationlog/1_targeting_rule/1_rule_no_targeted_attribute.txt new file mode 100644 index 0000000..80702e9 --- /dev/null +++ b/tests/data/evaluationlog/1_targeting_rule/1_rule_no_targeted_attribute.txt @@ -0,0 +1,6 @@ +WARNING [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/tests/data/evaluationlog/1_targeting_rule/1_rule_no_user.txt b/tests/data/evaluationlog/1_targeting_rule/1_rule_no_user.txt new file mode 100644 index 0000000..20e290f --- /dev/null +++ b/tests/data/evaluationlog/1_targeting_rule/1_rule_no_user.txt @@ -0,0 +1,6 @@ +WARNING [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 `getValue()` 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/tests/data/evaluationlog/1_targeting_rule/1_rule_not_matching_targeted_attribute.txt b/tests/data/evaluationlog/1_targeting_rule/1_rule_not_matching_targeted_attribute.txt new file mode 100644 index 0000000..49d1252 --- /dev/null +++ b/tests/data/evaluationlog/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/tests/data/evaluationlog/2_targeting_rules.json b/tests/data/evaluationlog/2_targeting_rules.json new file mode 100644 index 0000000..5cf8a3c --- /dev/null +++ b/tests/data/evaluationlog/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/tests/data/evaluationlog/2_targeting_rules/2_rules_matching_targeted_attribute.txt b/tests/data/evaluationlog/2_targeting_rules/2_rules_matching_targeted_attribute.txt new file mode 100644 index 0000000..d124a4f --- /dev/null +++ b/tests/data/evaluationlog/2_targeting_rules/2_rules_matching_targeted_attribute.txt @@ -0,0 +1,7 @@ +WARNING [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/tests/data/evaluationlog/2_targeting_rules/2_rules_no_targeted_attribute.txt b/tests/data/evaluationlog/2_targeting_rules/2_rules_no_targeted_attribute.txt new file mode 100644 index 0000000..0e02076 --- /dev/null +++ b/tests/data/evaluationlog/2_targeting_rules/2_rules_no_targeted_attribute.txt @@ -0,0 +1,9 @@ +WARNING [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/ +WARNING [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/tests/data/evaluationlog/2_targeting_rules/2_rules_no_user.txt b/tests/data/evaluationlog/2_targeting_rules/2_rules_no_user.txt new file mode 100644 index 0000000..9674ea8 --- /dev/null +++ b/tests/data/evaluationlog/2_targeting_rules/2_rules_no_user.txt @@ -0,0 +1,8 @@ +WARNING [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 `getValue()` 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/tests/data/evaluationlog/2_targeting_rules/2_rules_not_matching_targeted_attribute.txt b/tests/data/evaluationlog/2_targeting_rules/2_rules_not_matching_targeted_attribute.txt new file mode 100644 index 0000000..72217b2 --- /dev/null +++ b/tests/data/evaluationlog/2_targeting_rules/2_rules_not_matching_targeted_attribute.txt @@ -0,0 +1,7 @@ +WARNING [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/tests/data/evaluationlog/_overrides/test_list_truncation.json b/tests/data/evaluationlog/_overrides/test_list_truncation.json new file mode 100644 index 0000000..6fdde45 --- /dev/null +++ b/tests/data/evaluationlog/_overrides/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/tests/data/evaluationlog/and_rules.json b/tests/data/evaluationlog/and_rules.json new file mode 100644 index 0000000..c6ed879 --- /dev/null +++ b/tests/data/evaluationlog/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/tests/data/evaluationlog/and_rules/and_rules_no_user.txt b/tests/data/evaluationlog/and_rules/and_rules_no_user.txt new file mode 100644 index 0000000..0b8fb25 --- /dev/null +++ b/tests/data/evaluationlog/and_rules/and_rules_no_user.txt @@ -0,0 +1,7 @@ +WARNING [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 `getValue()` 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/tests/data/evaluationlog/and_rules/and_rules_user.txt b/tests/data/evaluationlog/and_rules/and_rules_user.txt new file mode 100644 index 0000000..92c59ce --- /dev/null +++ b/tests/data/evaluationlog/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/tests/data/evaluationlog/comparators.json b/tests/data/evaluationlog/comparators.json new file mode 100644 index 0000000..5d5631e --- /dev/null +++ b/tests/data/evaluationlog/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/tests/data/evaluationlog/comparators/allinone.txt b/tests/data/evaluationlog/comparators/allinone.txt new file mode 100644 index 0000000..84e9b32 --- /dev/null +++ b/tests/data/evaluationlog/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/tests/data/evaluationlog/epoch_date_validation.json b/tests/data/evaluationlog/epoch_date_validation.json new file mode 100644 index 0000000..e916d21 --- /dev/null +++ b/tests/data/evaluationlog/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/tests/data/evaluationlog/epoch_date_validation/date_error.txt b/tests/data/evaluationlog/epoch_date_validation/date_error.txt new file mode 100644 index 0000000..610b8f5 --- /dev/null +++ b/tests/data/evaluationlog/epoch_date_validation/date_error.txt @@ -0,0 +1,7 @@ +WARNING [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/tests/data/evaluationlog/list_truncation.json b/tests/data/evaluationlog/list_truncation.json new file mode 100644 index 0000000..64e9426 --- /dev/null +++ b/tests/data/evaluationlog/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/tests/data/evaluationlog/list_truncation/list_truncation.txt b/tests/data/evaluationlog/list_truncation/list_truncation.txt new file mode 100644 index 0000000..10a0195 --- /dev/null +++ b/tests/data/evaluationlog/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/tests/data/evaluationlog/number_validation.json b/tests/data/evaluationlog/number_validation.json new file mode 100644 index 0000000..640cf3d --- /dev/null +++ b/tests/data/evaluationlog/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/tests/data/evaluationlog/number_validation/number_error.txt b/tests/data/evaluationlog/number_validation/number_error.txt new file mode 100644 index 0000000..f936809 --- /dev/null +++ b/tests/data/evaluationlog/number_validation/number_error.txt @@ -0,0 +1,6 @@ +WARNING [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/tests/data/evaluationlog/options_after_targeting_rule.json b/tests/data/evaluationlog/options_after_targeting_rule.json new file mode 100644 index 0000000..803840e --- /dev/null +++ b/tests/data/evaluationlog/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/tests/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_matching_targeted_attribute.txt b/tests/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_matching_targeted_attribute.txt new file mode 100644 index 0000000..6815fa3 --- /dev/null +++ b/tests/data/evaluationlog/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/tests/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_no_targeted_attribute.txt b/tests/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_no_targeted_attribute.txt new file mode 100644 index 0000000..8e6facb --- /dev/null +++ b/tests/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_no_targeted_attribute.txt @@ -0,0 +1,9 @@ +WARNING [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/tests/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_no_user.txt b/tests/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_no_user.txt new file mode 100644 index 0000000..45f6354 --- /dev/null +++ b/tests/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_no_user.txt @@ -0,0 +1,7 @@ +WARNING [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 `getValue()` 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/tests/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_not_matching_targeted_attribute.txt b/tests/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_not_matching_targeted_attribute.txt new file mode 100644 index 0000000..c412e5a --- /dev/null +++ b/tests/data/evaluationlog/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/tests/data/evaluationlog/options_based_on_custom_attr.json b/tests/data/evaluationlog/options_based_on_custom_attr.json new file mode 100644 index 0000000..5f8d1c6 --- /dev/null +++ b/tests/data/evaluationlog/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/tests/data/evaluationlog/options_based_on_custom_attr/matching_options_custom_attribute.txt b/tests/data/evaluationlog/options_based_on_custom_attr/matching_options_custom_attribute.txt new file mode 100644 index 0000000..2621086 --- /dev/null +++ b/tests/data/evaluationlog/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/tests/data/evaluationlog/options_based_on_custom_attr/no_options_custom_attribute.txt b/tests/data/evaluationlog/options_based_on_custom_attr/no_options_custom_attribute.txt new file mode 100644 index 0000000..c92c5bc --- /dev/null +++ b/tests/data/evaluationlog/options_based_on_custom_attr/no_options_custom_attribute.txt @@ -0,0 +1,4 @@ +WARNING [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/tests/data/evaluationlog/options_based_on_custom_attr/options_custom_attribute_no_user.txt b/tests/data/evaluationlog/options_based_on_custom_attr/options_custom_attribute_no_user.txt new file mode 100644 index 0000000..9b6ec39 --- /dev/null +++ b/tests/data/evaluationlog/options_based_on_custom_attr/options_custom_attribute_no_user.txt @@ -0,0 +1,4 @@ +WARNING [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 `getValue()` 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/tests/data/evaluationlog/options_based_on_user_id.json b/tests/data/evaluationlog/options_based_on_user_id.json new file mode 100644 index 0000000..442f575 --- /dev/null +++ b/tests/data/evaluationlog/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/tests/data/evaluationlog/options_based_on_user_id/options_user_attribute_no_user.txt b/tests/data/evaluationlog/options_based_on_user_id/options_user_attribute_no_user.txt new file mode 100644 index 0000000..7d3116e --- /dev/null +++ b/tests/data/evaluationlog/options_based_on_user_id/options_user_attribute_no_user.txt @@ -0,0 +1,4 @@ +WARNING [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 `getValue()` 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/tests/data/evaluationlog/options_based_on_user_id/options_user_attribute_user.txt b/tests/data/evaluationlog/options_based_on_user_id/options_user_attribute_user.txt new file mode 100644 index 0000000..dac8dd6 --- /dev/null +++ b/tests/data/evaluationlog/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/tests/data/evaluationlog/options_within_targeting_rule.json b/tests/data/evaluationlog/options_within_targeting_rule.json new file mode 100644 index 0000000..4c6c533 --- /dev/null +++ b/tests/data/evaluationlog/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/tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_no_options_attribute.txt b/tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_no_options_attribute.txt new file mode 100644 index 0000000..db721f5 --- /dev/null +++ b/tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_no_options_attribute.txt @@ -0,0 +1,7 @@ +WARNING [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/tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_options_attribute.txt b/tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_options_attribute.txt new file mode 100644 index 0000000..8129521 --- /dev/null +++ b/tests/data/evaluationlog/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/tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_no_targeted_attribute.txt b/tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_no_targeted_attribute.txt new file mode 100644 index 0000000..74f812f --- /dev/null +++ b/tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_no_targeted_attribute.txt @@ -0,0 +1,6 @@ +WARNING [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/tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_no_user.txt b/tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_no_user.txt new file mode 100644 index 0000000..ecb6047 --- /dev/null +++ b/tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_no_user.txt @@ -0,0 +1,6 @@ +WARNING [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 `getValue()` 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/tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_not_matching_targeted_attribute.txt b/tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_not_matching_targeted_attribute.txt new file mode 100644 index 0000000..dd6032e --- /dev/null +++ b/tests/data/evaluationlog/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/tests/data/evaluationlog/prerequisite_flag.json b/tests/data/evaluationlog/prerequisite_flag.json new file mode 100644 index 0000000..9c35c00 --- /dev/null +++ b/tests/data/evaluationlog/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/tests/data/evaluationlog/prerequisite_flag/prerequisite_flag.txt b/tests/data/evaluationlog/prerequisite_flag/prerequisite_flag.txt new file mode 100644 index 0000000..1d9022b --- /dev/null +++ b/tests/data/evaluationlog/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/tests/data/evaluationlog/prerequisite_flag/prerequisite_flag_multilevel.txt b/tests/data/evaluationlog/prerequisite_flag/prerequisite_flag_multilevel.txt new file mode 100644 index 0000000..e9b9da6 --- /dev/null +++ b/tests/data/evaluationlog/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/tests/data/evaluationlog/prerequisite_flag/prerequisite_flag_no_user_needed_by_both.txt b/tests/data/evaluationlog/prerequisite_flag/prerequisite_flag_no_user_needed_by_both.txt new file mode 100644 index 0000000..cbb04c7 --- /dev/null +++ b/tests/data/evaluationlog/prerequisite_flag/prerequisite_flag_no_user_needed_by_both.txt @@ -0,0 +1,38 @@ +WARNING [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 `getValue()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +WARNING [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 `getValue()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +WARNING [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 `getValue()` 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/tests/data/evaluationlog/prerequisite_flag/prerequisite_flag_no_user_needed_by_dep.txt b/tests/data/evaluationlog/prerequisite_flag/prerequisite_flag_no_user_needed_by_dep.txt new file mode 100644 index 0000000..c86dc81 --- /dev/null +++ b/tests/data/evaluationlog/prerequisite_flag/prerequisite_flag_no_user_needed_by_dep.txt @@ -0,0 +1,15 @@ +WARNING [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 `getValue()` 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/tests/data/evaluationlog/prerequisite_flag/prerequisite_flag_no_user_needed_by_prereq.txt b/tests/data/evaluationlog/prerequisite_flag/prerequisite_flag_no_user_needed_by_prereq.txt new file mode 100644 index 0000000..1796e7c --- /dev/null +++ b/tests/data/evaluationlog/prerequisite_flag/prerequisite_flag_no_user_needed_by_prereq.txt @@ -0,0 +1,18 @@ +WARNING [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 `getValue()` 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/tests/data/evaluationlog/segment.json b/tests/data/evaluationlog/segment.json new file mode 100644 index 0000000..1bb4df5 --- /dev/null +++ b/tests/data/evaluationlog/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/tests/data/evaluationlog/segment/segment_matching.txt b/tests/data/evaluationlog/segment/segment_matching.txt new file mode 100644 index 0000000..9065aae --- /dev/null +++ b/tests/data/evaluationlog/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/tests/data/evaluationlog/segment/segment_no_matching.txt b/tests/data/evaluationlog/segment/segment_no_matching.txt new file mode 100644 index 0000000..0d04d83 --- /dev/null +++ b/tests/data/evaluationlog/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/tests/data/evaluationlog/segment/segment_no_targeted_attribute.txt b/tests/data/evaluationlog/segment/segment_no_targeted_attribute.txt new file mode 100644 index 0000000..6c7cf7e --- /dev/null +++ b/tests/data/evaluationlog/segment/segment_no_targeted_attribute.txt @@ -0,0 +1,13 @@ +WARNING [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/tests/data/evaluationlog/segment/segment_no_user.txt b/tests/data/evaluationlog/segment/segment_no_user.txt new file mode 100644 index 0000000..e8bd540 --- /dev/null +++ b/tests/data/evaluationlog/segment/segment_no_user.txt @@ -0,0 +1,6 @@ +WARNING [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 `getValue()` 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/tests/data/evaluationlog/segment/segment_no_user_multi_conditions.txt b/tests/data/evaluationlog/segment/segment_no_user_multi_conditions.txt new file mode 100644 index 0000000..f09f9ce --- /dev/null +++ b/tests/data/evaluationlog/segment/segment_no_user_multi_conditions.txt @@ -0,0 +1,7 @@ +WARNING [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 `getValue()` 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/tests/data/evaluationlog/semver_validation.json b/tests/data/evaluationlog/semver_validation.json new file mode 100644 index 0000000..3a14fc6 --- /dev/null +++ b/tests/data/evaluationlog/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/tests/data/evaluationlog/semver_validation/semver_error.txt b/tests/data/evaluationlog/semver_validation/semver_error.txt new file mode 100644 index 0000000..e14cc95 --- /dev/null +++ b/tests/data/evaluationlog/semver_validation/semver_error.txt @@ -0,0 +1,9 @@ +WARNING [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. +WARNING [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/tests/data/evaluationlog/semver_validation/semver_relations_error.txt b/tests/data/evaluationlog/semver_validation/semver_relations_error.txt new file mode 100644 index 0000000..8198c85 --- /dev/null +++ b/tests/data/evaluationlog/semver_validation/semver_relations_error.txt @@ -0,0 +1,18 @@ +WARNING [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. +WARNING [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. +WARNING [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. +WARNING [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. +WARNING [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/tests/data/evaluationlog/simple_value.json b/tests/data/evaluationlog/simple_value.json new file mode 100644 index 0000000..070d6f5 --- /dev/null +++ b/tests/data/evaluationlog/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/tests/data/evaluationlog/simple_value/double_setting.txt b/tests/data/evaluationlog/simple_value/double_setting.txt new file mode 100644 index 0000000..4a632f7 --- /dev/null +++ b/tests/data/evaluationlog/simple_value/double_setting.txt @@ -0,0 +1,2 @@ +INFO [5000] Evaluating 'doubleDefaultPi' + Returning '3.1415'. diff --git a/tests/data/evaluationlog/simple_value/int_setting.txt b/tests/data/evaluationlog/simple_value/int_setting.txt new file mode 100644 index 0000000..1361843 --- /dev/null +++ b/tests/data/evaluationlog/simple_value/int_setting.txt @@ -0,0 +1,2 @@ +INFO [5000] Evaluating 'integerDefaultOne' + Returning '1'. diff --git a/tests/data/evaluationlog/simple_value/off_flag.txt b/tests/data/evaluationlog/simple_value/off_flag.txt new file mode 100644 index 0000000..4580685 --- /dev/null +++ b/tests/data/evaluationlog/simple_value/off_flag.txt @@ -0,0 +1,2 @@ +INFO [5000] Evaluating 'boolDefaultFalse' + Returning 'false'. diff --git a/tests/data/evaluationlog/simple_value/on_flag.txt b/tests/data/evaluationlog/simple_value/on_flag.txt new file mode 100644 index 0000000..274c990 --- /dev/null +++ b/tests/data/evaluationlog/simple_value/on_flag.txt @@ -0,0 +1,2 @@ +INFO [5000] Evaluating 'boolDefaultTrue' + Returning 'true'. diff --git a/tests/data/evaluationlog/simple_value/text_setting.txt b/tests/data/evaluationlog/simple_value/text_setting.txt new file mode 100644 index 0000000..831d7c6 --- /dev/null +++ b/tests/data/evaluationlog/simple_value/text_setting.txt @@ -0,0 +1,2 @@ +INFO [5000] Evaluating 'stringDefaultCat' + Returning 'Cat'. diff --git a/tests/data/test-rules.json b/tests/data/test-rules.json new file mode 100644 index 0000000..7d59a2d --- /dev/null +++ b/tests/data/test-rules.json @@ -0,0 +1,30 @@ +{ + "f": { + "rolloutFeature": { + "t": 0, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 2, + "l": [ + "@example.com" + ] + } + } + ], + "s": { + "v": { + "b": true + } + } + } + ], + "v": { + "b": false + } + } + } +} \ No newline at end of file diff --git a/tests/test-simple.json b/tests/data/test-simple.json similarity index 100% rename from tests/test-simple.json rename to tests/data/test-simple.json diff --git a/tests/data/test.json b/tests/data/test.json new file mode 100644 index 0000000..8f1db8a --- /dev/null +++ b/tests/data/test.json @@ -0,0 +1,34 @@ +{ + "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" + } + } + } +} diff --git a/tests/data/test_circulardependency_v6.json b/tests/data/test_circulardependency_v6.json new file mode 100644 index 0000000..a8a9e17 --- /dev/null +++ b/tests/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/tests/data/test_override_flagdependency_v6.json b/tests/data/test_override_flagdependency_v6.json new file mode 100644 index 0000000..62e159e --- /dev/null +++ b/tests/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/tests/data/test_override_segments_v6.json b/tests/data/test_override_segments_v6.json new file mode 100644 index 0000000..47bf15c --- /dev/null +++ b/tests/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/tests/testmatrix.csv b/tests/data/testmatrix.csv similarity index 84% rename from tests/testmatrix.csv rename to tests/data/testmatrix.csv index 6d03876..6de7454 100644 --- a/tests/testmatrix.csv +++ b/tests/data/testmatrix.csv @@ -1,5 +1,5 @@ Identifier;Email;Country;Custom1;bool30TrueAdvancedRules;boolDefaultFalse;boolDefaultTrue;double25Pi25E25Gr25Zero;doubleDefaultPi;integer25One25Two25Three25FourAdvancedRules;integerDefaultOne;string25Cat25Dog25Falcon25Horse;string25Cat25Dog25Falcon25HorseAdvancedRules;string75Cat0Dog25Falcon0Horse;stringContainsDogDefaultCat;stringDefaultCat;stringIsInDogDefaultCat;stringIsNotInDogDefaultCat;stringNotContainsDogDefaultCat -##null##;;;;True;False;True;-1.0;3.1415;-1;1;Chicken;Chicken;Chicken;Cat;Cat;Cat;Cat;Cat +##null##;;;;True;False;True;-1;3.1415;-1;1;Chicken;Chicken;Chicken;Cat;Cat;Cat;Cat;Cat ;;;;False;False;True;2.7182;3.1415;4;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat a@configcat.com;a@configcat.com;Hungary;admin;False;False;True;5.561;3.1415;5;1;Cat;Dolphin;Cat;Dog;Cat;Dog;Cat;Cat b@configcat.com;b@configcat.com;Hungary;;False;False;True;5.561;3.1415;5;1;Falcon;Dolphin;Cat;Dog;Cat;Dog;Cat;Cat @@ -12,9 +12,9 @@ h@configcat.com;h@configcat.com;;;False;False;True;5.561;3.1415;5;1;Cat;Kitten;C i@configcat.com;i@configcat.com;;admin;True;False;True;5.561;3.1415;5;1;Cat;Lion;Falcon;Dog;Cat;Dog;Dog;Cat j@configcat.com;j@configcat.com;;;False;False;True;5.561;3.1415;5;1;Cat;Kitten;Falcon;Dog;Cat;Cat;Dog;Cat stern@msn.com;stern@msn.com;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Dog;Dog -sarahs@yahoo.com;sarahs@yahoo.com;##null##;##null##;True;False;True;0.0;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog +sarahs@yahoo.com;sarahs@yahoo.com;##null##;##null##;True;False;True;0;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog luebke@hotmail.com;luebke@hotmail.com;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Cat;Cat;Cat;Cat;Cat;Dog;Dog -padme@icloud.com;padme@icloud.com;##null##;##null##;True;False;True;0.0;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Dog;Dog +padme@icloud.com;padme@icloud.com;##null##;##null##;True;False;True;0;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Dog;Dog claypool@aol.com;claypool@aol.com;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Dog;Dog bogjobber@verizon.net;bogjobber@verizon.net;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Dog;Horse;Falcon;Cat;Cat;Cat;Dog;Dog cliffordj@aol.com;cliffordj@aol.com;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Horse;Horse;Cat;Cat;Cat;Cat;Dog;Dog @@ -29,57 +29,57 @@ josem@icloud.com;josem@icloud.com;##null##;##null##;False;False;True;2.7182;3.14 hedwig@outlook.com;hedwig@outlook.com;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Horse;Horse;Falcon;Cat;Cat;Cat;Dog;Dog camenisch@yahoo.com;camenisch@yahoo.com;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog ccohen@comcast.net;ccohen@comcast.net;##null##;##null##;True;False;True;3.1415;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Dog;Dog -techie@att.net;techie@att.net;##null##;##null##;False;False;True;0.0;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog +techie@att.net;techie@att.net;##null##;##null##;False;False;True;0;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog damian@gmail.com;damian@gmail.com;##null##;##null##;True;False;True;3.1415;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Dog;Dog -psharpe@comcast.net;psharpe@comcast.net;##null##;##null##;False;False;True;0.0;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog +psharpe@comcast.net;psharpe@comcast.net;##null##;##null##;False;False;True;0;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog ebassi@me.com;ebassi@me.com;##null##;##null##;True;False;True;3.1415;3.1415;3;1;Horse;Horse;Falcon;Cat;Cat;Cat;Dog;Dog curly@aol.com;curly@aol.com;##null##;##null##;True;False;True;3.1415;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Dog;Dog rddesign@optonline.net;rddesign@optonline.net;##null##;##null##;True;False;True;3.1415;3.1415;4;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Dog;Dog boftx@gmail.com;boftx@gmail.com;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Dog;Dog eegsa@yahoo.ca;eegsa@yahoo.ca;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Dog;Dog ganter@gmail.com;ganter@gmail.com;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Dog;Dog -mleary@att.net;mleary@att.net;##null##;##null##;False;False;True;0.0;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Dog;Dog +mleary@att.net;mleary@att.net;##null##;##null##;False;False;True;0;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Dog;Dog kassiesa@icloud.com;kassiesa@icloud.com;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog -peterhoeg@outlook.com;peterhoeg@outlook.com;##null##;##null##;False;False;True;0.0;3.1415;4;1;Dog;Cat;Cat;Cat;Cat;Cat;Dog;Dog +peterhoeg@outlook.com;peterhoeg@outlook.com;##null##;##null##;False;False;True;0;3.1415;4;1;Dog;Cat;Cat;Cat;Cat;Cat;Dog;Dog mhanoh@yahoo.ca;mhanoh@yahoo.ca;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog -henkp@yahoo.com;henkp@yahoo.com;##null##;##null##;False;False;True;0.0;3.1415;1;1;Horse;Horse;Falcon;Cat;Cat;Cat;Dog;Dog -krueger@sbcglobal.net;krueger@sbcglobal.net;##null##;##null##;False;False;True;0.0;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Dog;Dog +henkp@yahoo.com;henkp@yahoo.com;##null##;##null##;False;False;True;0;3.1415;1;1;Horse;Horse;Falcon;Cat;Cat;Cat;Dog;Dog +krueger@sbcglobal.net;krueger@sbcglobal.net;##null##;##null##;False;False;True;0;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Dog;Dog barjam@yahoo.com;barjam@yahoo.com;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Horse;Horse;Falcon;Cat;Cat;Cat;Dog;Dog mirod@msn.com;mirod@msn.com;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Dog;Dog -marioph@yahoo.com;marioph@yahoo.com;##null##;##null##;False;False;True;0.0;3.1415;2;1;Cat;Dog;Cat;Cat;Cat;Cat;Dog;Dog +marioph@yahoo.com;marioph@yahoo.com;##null##;##null##;False;False;True;0;3.1415;2;1;Cat;Dog;Cat;Cat;Cat;Cat;Dog;Dog niknejad@optonline.net;niknejad@optonline.net;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Dog;Dog bwcarty@sbcglobal.net;bwcarty@sbcglobal.net;##null##;##null##;True;False;True;3.1415;3.1415;4;1;Dog;Horse;Falcon;Cat;Cat;Cat;Dog;Dog mcast@aol.com;mcast@aol.com;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Dog;Dog -portscan@msn.com;portscan@msn.com;##null##;##null##;False;False;True;0.0;3.1415;4;1;Falcon;Horse;Cat;Cat;Cat;Cat;Dog;Dog +portscan@msn.com;portscan@msn.com;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Horse;Cat;Cat;Cat;Cat;Dog;Dog pereinar@yahoo.ca;pereinar@yahoo.ca;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog floxy@verizon.net;floxy@verizon.net;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Falcon;Horse;Cat;Cat;Cat;Cat;Dog;Dog -mhassel@comcast.net;mhassel@comcast.net;##null##;##null##;False;False;True;0.0;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Dog;Dog +mhassel@comcast.net;mhassel@comcast.net;##null##;##null##;False;False;True;0;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Dog;Dog mgemmons@optonline.net;mgemmons@optonline.net;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Dog;Cat;Falcon;Cat;Cat;Cat;Dog;Dog -luvirini@mac.com;luvirini@mac.com;##null##;##null##;False;False;True;0.0;3.1415;4;1;Dog;Horse;Falcon;Cat;Cat;Cat;Dog;Dog +luvirini@mac.com;luvirini@mac.com;##null##;##null##;False;False;True;0;3.1415;4;1;Dog;Horse;Falcon;Cat;Cat;Cat;Dog;Dog gslondon@gmail.com;gslondon@gmail.com;##null##;##null##;True;False;True;3.1415;3.1415;2;1;Cat;Horse;Cat;Cat;Cat;Cat;Dog;Dog lamky@comcast.net;lamky@comcast.net;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Dog;Dog -lipeng@aol.com;lipeng@aol.com;##null##;##null##;False;False;True;0.0;3.1415;4;1;Cat;Horse;Cat;Cat;Cat;Cat;Dog;Dog +lipeng@aol.com;lipeng@aol.com;##null##;##null##;False;False;True;0;3.1415;4;1;Cat;Horse;Cat;Cat;Cat;Cat;Dog;Dog keiji@mac.com;keiji@mac.com;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Dog;Dog;Falcon;Cat;Cat;Cat;Dog;Dog gumpish@verizon.net;gumpish@verizon.net;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Dog;Dog tromey@hotmail.com;tromey@hotmail.com;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Dog;Dog miyop@aol.com;miyop@aol.com;##null##;##null##;True;False;True;3.1415;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Dog;Dog natepuri@me.com;natepuri@me.com;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog sbmrjbr@outlook.com;sbmrjbr@outlook.com;##null##;##null##;True;False;True;3.1415;3.1415;4;1;Horse;Dog;Falcon;Cat;Cat;Cat;Dog;Dog -hahiss@gmail.com;hahiss@gmail.com;##null##;##null##;False;False;True;0.0;3.1415;1;1;Horse;Horse;Cat;Cat;Cat;Cat;Dog;Dog -gmcgath@yahoo.ca;gmcgath@yahoo.ca;##null##;##null##;True;False;True;0.0;3.1415;1;1;Falcon;Dog;Cat;Cat;Cat;Cat;Dog;Dog -zavadsky@msn.com;zavadsky@msn.com;##null##;##null##;True;False;True;0.0;3.1415;3;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Dog;Dog -munson@gmail.com;munson@gmail.com;##null##;##null##;False;False;True;0.0;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Dog;Dog -jfriedl@yahoo.com;jfriedl@yahoo.com;##null##;##null##;False;False;True;0.0;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog +hahiss@gmail.com;hahiss@gmail.com;##null##;##null##;False;False;True;0;3.1415;1;1;Horse;Horse;Cat;Cat;Cat;Cat;Dog;Dog +gmcgath@yahoo.ca;gmcgath@yahoo.ca;##null##;##null##;True;False;True;0;3.1415;1;1;Falcon;Dog;Cat;Cat;Cat;Cat;Dog;Dog +zavadsky@msn.com;zavadsky@msn.com;##null##;##null##;True;False;True;0;3.1415;3;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Dog;Dog +munson@gmail.com;munson@gmail.com;##null##;##null##;False;False;True;0;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Dog;Dog +jfriedl@yahoo.com;jfriedl@yahoo.com;##null##;##null##;False;False;True;0;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog lushe@yahoo.ca;lushe@yahoo.ca;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Cat;Cat;Falcon;Cat;Cat;Cat;Dog;Dog skythe@gmail.com;skythe@gmail.com;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Horse;Horse;Falcon;Cat;Cat;Cat;Dog;Dog -lipeng@aol.com;lipeng@aol.com;##null##;##null##;False;False;True;0.0;3.1415;4;1;Cat;Horse;Cat;Cat;Cat;Cat;Dog;Dog +lipeng@aol.com;lipeng@aol.com;##null##;##null##;False;False;True;0;3.1415;4;1;Cat;Horse;Cat;Cat;Cat;Cat;Dog;Dog jigsaw@me.com;jigsaw@me.com;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Falcon;Horse;Cat;Cat;Cat;Cat;Dog;Dog schwaang@gmail.com;schwaang@gmail.com;##null##;##null##;True;False;True;1.61803;3.1415;4;1;Horse;Dog;Cat;Cat;Cat;Cat;Dog;Dog eurohack@verizon.net;eurohack@verizon.net;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Falcon;Dog;Cat;Cat;Cat;Cat;Dog;Dog janneh@icloud.com;janneh@icloud.com;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Dog;Horse;Falcon;Cat;Cat;Cat;Dog;Dog -frederic@me.com;frederic@me.com;##null##;##null##;False;False;True;0.0;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Dog;Dog +frederic@me.com;frederic@me.com;##null##;##null##;False;False;True;0;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Dog;Dog facet@optonline.net;facet@optonline.net;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Dog;Dog -uncle@aol.com;uncle@aol.com;##null##;##null##;False;False;True;0.0;3.1415;3;1;Horse;Horse;Falcon;Cat;Cat;Cat;Dog;Dog +uncle@aol.com;uncle@aol.com;##null##;##null##;False;False;True;0;3.1415;3;1;Horse;Horse;Falcon;Cat;Cat;Cat;Dog;Dog wilsonpm@comcast.net;wilsonpm@comcast.net;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Dog;Dog garland@optonline.net;garland@optonline.net;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Dog;Dog srour@yahoo.com;srour@yahoo.com;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Dog;Dog;Cat;Cat;Cat;Cat;Dog;Dog @@ -91,9 +91,9 @@ bester@mac.com;bester@mac.com;##null##;##null##;True;False;True;1.61803;3.1415;1 kildjean@verizon.net;kildjean@verizon.net;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Cat;Horse;Falcon;Cat;Cat;Cat;Dog;Dog arandal@comcast.net;arandal@comcast.net;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Dog;Dog bartlett@yahoo.com;bartlett@yahoo.com;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Dog;Falcon;Cat;Cat;Cat;Cat;Dog;Dog -zyghom@icloud.com;zyghom@icloud.com;##null##;##null##;False;False;True;0.0;3.1415;3;1;Falcon;Horse;Cat;Cat;Cat;Cat;Dog;Dog +zyghom@icloud.com;zyghom@icloud.com;##null##;##null##;False;False;True;0;3.1415;3;1;Falcon;Horse;Cat;Cat;Cat;Cat;Dog;Dog valdez@mac.com;valdez@mac.com;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Dog;Dog -scato@yahoo.com;scato@yahoo.com;##null##;##null##;False;False;True;0.0;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Dog;Dog +scato@yahoo.com;scato@yahoo.com;##null##;##null##;False;False;True;0;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Dog;Dog sinkou@live.com;sinkou@live.com;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Dog;Falcon;Falcon;Cat;Cat;Cat;Dog;Dog evilopie@comcast.net;evilopie@comcast.net;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Dog;Dog ducasse@gmail.com;ducasse@gmail.com;##null##;##null##;True;False;True;3.1415;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Dog;Dog @@ -101,30 +101,30 @@ sthomas@sbcglobal.net;sthomas@sbcglobal.net;##null##;##null##;False;False;True;1 plover@msn.com;plover@msn.com;##null##;##null##;True;False;True;3.1415;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Dog;Dog mavilar@yahoo.com;mavilar@yahoo.com;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Dog;Dog josephw@msn.com;josephw@msn.com;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Dog;Dog -qmacro@yahoo.com;qmacro@yahoo.com;##null##;##null##;True;False;True;0.0;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Dog;Dog +qmacro@yahoo.com;qmacro@yahoo.com;##null##;##null##;True;False;True;0;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Dog;Dog munson@mac.com;munson@mac.com;##null##;##null##;True;False;True;3.1415;3.1415;3;1;Cat;Cat;Falcon;Cat;Cat;Cat;Dog;Dog paulv@mac.com;paulv@mac.com;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Dog;Dog dogdude@hotmail.com;dogdude@hotmail.com;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Cat;Dog;Falcon;Cat;Cat;Cat;Dog;Dog -symbolic@yahoo.ca;symbolic@yahoo.ca;##null##;##null##;False;False;True;0.0;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Dog;Dog +symbolic@yahoo.ca;symbolic@yahoo.ca;##null##;##null##;False;False;True;0;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Dog;Dog carcus@yahoo.com;carcus@yahoo.com;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Dog;Dog sblack@me.com;sblack@me.com;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Dog;Dog;Falcon;Cat;Cat;Cat;Dog;Dog richard@gmail.com;richard@gmail.com;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Falcon;Dog;Cat;Cat;Cat;Cat;Dog;Dog tbusch@yahoo.ca;tbusch@yahoo.ca;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Dog;Dog;Cat;Cat;Cat;Cat;Dog;Dog gtaylor@aol.com;gtaylor@aol.com;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Dog;Dog burniske@att.net;burniske@att.net;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Cat;Horse;Cat;Cat;Cat;Cat;Dog;Dog -bebing@me.com;bebing@me.com;##null##;##null##;False;False;True;0.0;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Dog;Dog +bebing@me.com;bebing@me.com;##null##;##null##;False;False;True;0;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Dog;Dog joglo@gmail.com;joglo@gmail.com;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Dog;Dog chrwin@sbcglobal.net;chrwin@sbcglobal.net;##null##;##null##;True;False;True;3.1415;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog chaikin@yahoo.com;chaikin@yahoo.com;##null##;##null##;True;False;True;3.1415;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Dog;Dog -jigsaw@verizon.net;jigsaw@verizon.net;##null##;##null##;True;False;True;0.0;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Dog;Dog +jigsaw@verizon.net;jigsaw@verizon.net;##null##;##null##;True;False;True;0;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Dog;Dog wbarker@yahoo.ca;wbarker@yahoo.ca;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog ganter@verizon.net;ganter@verizon.net;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Dog;Dog -eegsa@att.net;eegsa@att.net;##null##;##null##;False;False;True;0.0;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Dog;Dog +eegsa@att.net;eegsa@att.net;##null##;##null##;False;False;True;0;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Dog;Dog sethbrown@hotmail.com;sethbrown@hotmail.com;##null##;##null##;True;False;True;1.61803;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Dog;Dog solomon@me.com;solomon@me.com;##null##;##null##;True;False;True;3.1415;3.1415;2;1;Cat;Dog;Cat;Cat;Cat;Cat;Dog;Dog tellis@yahoo.ca;tellis@yahoo.ca;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Dog;Dog jshirley@optonline.net;jshirley@optonline.net;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Dog;Dog -tattooman@verizon.net;tattooman@verizon.net;##null##;##null##;False;False;True;0.0;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Dog;Dog +tattooman@verizon.net;tattooman@verizon.net;##null##;##null##;False;False;True;0;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Dog;Dog bescoto@yahoo.com;bescoto@yahoo.com;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Dog;Dog hstiles@comcast.net;hstiles@comcast.net;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Dog;Dog gumpish@optonline.net;gumpish@optonline.net;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Horse;Cat;Cat;Cat;Cat;Cat;Dog;Dog @@ -139,12 +139,12 @@ shrapnull@att.net;shrapnull@att.net;##null##;##null##;True;False;True;2.7182;3.1 lcheng@comcast.net;lcheng@comcast.net;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Dog;Falcon;Cat;Cat;Cat;Cat;Dog;Dog cyrus@msn.com;cyrus@msn.com;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Dog;Dog suresh@yahoo.ca;suresh@yahoo.ca;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Dog;Dog -elflord@yahoo.ca;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -sassen@verizon.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +elflord@yahoo.ca;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +sassen@verizon.net;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat dbindel@live.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -morain@hotmail.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +morain@hotmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat timtroyr@outlook.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;3;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -esbeck@live.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;1;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +esbeck@live.com;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat ilyaz@hotmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Horse;Cat;Falcon;Cat;Cat;Cat;Cat;Cat grinder@icloud.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat trieuvan@gmail.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat @@ -154,7 +154,7 @@ nichoj@outlook.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;2;1 sopwith@outlook.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat odlyzko@yahoo.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat warrior@optonline.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -budinger@msn.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +budinger@msn.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat lstein@comcast.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat kmiller@gmail.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;4;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat british@msn.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat @@ -162,19 +162,19 @@ webinc@gmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1; kohlis@aol.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat matthijs@outlook.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat mmccool@me.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -ribet@hotmail.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -wildfire@me.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +ribet@hotmail.com;##null##;##null##;##null##;True;False;True;0;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +wildfire@me.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat makarow@gmail.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat garland@hotmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -kjohnson@outlook.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Horse;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +kjohnson@outlook.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Horse;Cat;Falcon;Cat;Cat;Cat;Cat;Cat oneiros@sbcglobal.net;##null##;##null##;##null##;True;False;True;1.61803;3.1415;3;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -jaxweb@gmail.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat -raides@msn.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +jaxweb@gmail.com;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +raides@msn.com;##null##;##null##;##null##;True;False;True;0;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat cantu@comcast.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat msherr@comcast.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -dwsauder@aol.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +dwsauder@aol.com;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat comdig@gmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -esokullu@yahoo.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +esokullu@yahoo.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat kjetilk@aol.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Cat;Cat boomzilla@icloud.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat cvrcek@outlook.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat @@ -186,76 +186,76 @@ weidai@hotmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2; dpitts@live.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat bebing@optonline.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Dog;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat wikinerd@yahoo.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -pfitza@yahoo.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -policies@me.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +pfitza@yahoo.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +policies@me.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat niknejad@me.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat aukjan@hotmail.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat dleconte@sbcglobal.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Cat;Dog;Falcon;Cat;Cat;Cat;Cat;Cat -noahb@aol.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +noahb@aol.com;##null##;##null##;##null##;True;False;True;0;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat bdbrown@aol.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Horse;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -adillon@att.net;##null##;##null##;##null##;True;False;True;0.0;3.1415;3;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +adillon@att.net;##null##;##null##;##null##;True;False;True;0;3.1415;3;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat eegsa@me.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat chunzi@hotmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat privcan@optonline.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat mglee@hotmail.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat leocharre@me.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Cat;Dog;Falcon;Cat;Cat;Cat;Cat;Cat dwendlan@verizon.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -lpalmer@hotmail.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +lpalmer@hotmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat emcleod@msn.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat breegster@sbcglobal.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat mwandel@comcast.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -stewwy@me.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;2;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +stewwy@me.com;##null##;##null##;##null##;True;False;True;0;3.1415;2;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat drolsky@live.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -lukka@live.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;1;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +lukka@live.com;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat geekgrl@me.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat epeeist@me.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat papathan@verizon.net;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat singh@optonline.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -njpayne@aol.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +njpayne@aol.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat willg@comcast.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat jimmichie@icloud.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat frosal@aol.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat dunstan@yahoo.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat parasite@yahoo.ca;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -firstpr@msn.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;2;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +firstpr@msn.com;##null##;##null##;##null##;True;False;True;0;3.1415;2;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat konit@icloud.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat amaranth@msn.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat mcsporran@msn.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -gommix@yahoo.ca;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -dprice@verizon.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +gommix@yahoo.ca;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +dprice@verizon.net;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat lcheng@me.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -dwendlan@optonline.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -miami@hotmail.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +dwendlan@optonline.net;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +miami@hotmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat ajlitt@hotmail.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat grdschl@mac.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat andersbr@att.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat nacho@yahoo.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat hoangle@msn.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat jbuchana@gmail.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -knorr@sbcglobal.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Horse;Dog;Falcon;Cat;Cat;Cat;Cat;Cat -saridder@gmail.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;1;1;Horse;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -scotfl@outlook.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -skoch@yahoo.ca;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +knorr@sbcglobal.net;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Horse;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +saridder@gmail.com;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Horse;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +scotfl@outlook.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +skoch@yahoo.ca;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat luebke@verizon.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -bsikdar@live.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;3;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +bsikdar@live.com;##null##;##null##;##null##;True;False;True;0;3.1415;3;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat ryanvm@yahoo.ca;##null##;##null##;##null##;True;False;True;1.61803;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat dburrows@gmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -seebs@hotmail.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +seebs@hotmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat mgemmons@optonline.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Dog;Cat;Falcon;Cat;Cat;Cat;Cat;Cat kobayasi@att.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat marcs@sbcglobal.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -netsfr@att.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -martink@me.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +netsfr@att.net;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +martink@me.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat bflong@verizon.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat bhima@outlook.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;4;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat oster@att.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -teverett@yahoo.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +teverett@yahoo.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat kannan@optonline.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat yzheng@verizon.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat msusa@gmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -hmbrand@gmail.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +hmbrand@gmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat falcao@gmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -uraeus@live.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +uraeus@live.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat dunstan@msn.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat oracle@yahoo.ca;##null##;##null##;##null##;True;False;True;3.1415;3.1415;2;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat tbeck@gmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat @@ -265,25 +265,25 @@ yenya@msn.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Dog; bjoern@icloud.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat benanov@aol.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat preneel@outlook.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -punkis@sbcglobal.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -rwelty@comcast.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +punkis@sbcglobal.net;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +rwelty@comcast.net;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat penna@me.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat baveja@msn.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat louise@verizon.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Cat;Dog;Falcon;Cat;Cat;Cat;Cat;Cat arachne@icloud.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat hahiss@msn.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat wayward@optonline.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -pajas@sbcglobal.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -intlprog@comcast.net;##null##;##null##;##null##;True;False;True;0.0;3.1415;2;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +pajas@sbcglobal.net;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +intlprog@comcast.net;##null##;##null##;##null##;True;False;True;0;3.1415;2;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat hermanab@sbcglobal.net;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat presoff@msn.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat trygstad@mac.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -denton@optonline.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -skythe@live.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -hmbrand@gmail.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +denton@optonline.net;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +skythe@live.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +hmbrand@gmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat lushe@sbcglobal.net;##null##;##null##;##null##;True;False;True;1.61803;3.1415;2;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -magusnet@outlook.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -ullman@optonline.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +magusnet@outlook.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +ullman@optonline.net;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat jyoliver@optonline.net;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat flavell@icloud.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;1;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat ianbuck@att.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat @@ -292,11 +292,11 @@ gommix@sbcglobal.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4 rnelson@att.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat crusader@gmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat rddesign@sbcglobal.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -nanop@mac.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +nanop@mac.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat ngedmond@live.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;1;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat munjal@live.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat unreal@aol.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -jemarch@sbcglobal.net;##null##;##null##;##null##;True;False;True;0.0;3.1415;2;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +jemarch@sbcglobal.net;##null##;##null##;##null##;True;False;True;0;3.1415;2;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat shawnce@sbcglobal.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat nweaver@yahoo.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat british@verizon.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat @@ -310,10 +310,10 @@ mrobshaw@optonline.net;##null##;##null##;##null##;True;False;True;1.61803;3.1415 denton@yahoo.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat konst@mac.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat louise@aol.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -wetter@gmail.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +wetter@gmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat kohlis@att.net;##null##;##null##;##null##;True;False;True;3.1415;3.1415;4;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat monkeydo@aol.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat -melnik@yahoo.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +melnik@yahoo.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat munge@verizon.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat stefano@live.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat giafly@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat @@ -324,8 +324,8 @@ firstpr@yahoo.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1; nichoj@comcast.net;##null##;##null##;##null##;True;False;True;3.1415;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat aibrahim@optonline.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat inico@aol.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;4;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -ribet@sbcglobal.net;##null##;##null##;##null##;True;False;True;0.0;3.1415;1;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -ajlitt@mac.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +ribet@sbcglobal.net;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +ajlitt@mac.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat errxn@me.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat lstein@icloud.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat mgemmons@icloud.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Horse;Horse;Falcon;Cat;Cat;Cat;Cat;Cat @@ -340,17 +340,17 @@ dhwon@yahoo.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Do mstrout@msn.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat manuals@me.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat andrewik@me.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -hahsler@icloud.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;4;1;Dog;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +hahsler@icloud.com;##null##;##null##;##null##;True;False;True;0;3.1415;4;1;Dog;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat miami@verizon.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat facet@me.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -zeitlin@outlook.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -lamprecht@aol.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +zeitlin@outlook.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +lamprecht@aol.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat johnh@mac.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Dog;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -mrsam@yahoo.ca;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +mrsam@yahoo.ca;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat lipeng@outlook.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat dsowsy@icloud.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat philen@icloud.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -kjohnson@gmail.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +kjohnson@gmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat nelson@hotmail.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat syncnine@comcast.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat pgottsch@hotmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat @@ -360,16 +360,16 @@ mrdvt@outlook.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1; cfhsoft@outlook.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat kodeman@yahoo.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat mbrown@comcast.net;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -jaxweb@hotmail.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +jaxweb@hotmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat symbolic@icloud.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat denism@att.net;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat hager@mac.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -zavadsky@yahoo.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;2;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +zavadsky@yahoo.com;##null##;##null##;##null##;True;False;True;0;3.1415;2;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat mugwump@hotmail.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat geekgrl@comcast.net;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -dprice@me.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -petersko@yahoo.ca;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -netsfr@aol.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;2;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +dprice@me.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +petersko@yahoo.ca;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +netsfr@aol.com;##null##;##null##;##null##;True;False;True;0;3.1415;2;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat tbmaddux@aol.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat meder@att.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat benits@live.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat @@ -395,12 +395,12 @@ wayward@yahoo.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1; amaranth@me.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;2;1;Falcon;Dog;Falcon;Cat;Cat;Cat;Cat;Cat garland@yahoo.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;3;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat rfisher@live.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -stern@verizon.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +stern@verizon.net;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat mavilar@yahoo.ca;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat rfisher@sbcglobal.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Horse;Dog;Falcon;Cat;Cat;Cat;Cat;Cat tarreau@att.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -koudas@sbcglobal.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -bwcarty@mac.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;2;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +koudas@sbcglobal.net;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +bwcarty@mac.com;##null##;##null##;##null##;True;False;True;0;3.1415;2;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat jeteve@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat pmint@comcast.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat barlow@icloud.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat @@ -415,24 +415,24 @@ clkao@outlook.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1; noahb@yahoo.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat ducasse@comcast.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat jrkorson@live.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -hmbrand@gmail.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +hmbrand@gmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat corrada@yahoo.ca;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat tmaek@aol.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Horse;Cat;Falcon;Cat;Cat;Cat;Cat;Cat richard@mac.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat pkplex@comcast.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -kwilliams@icloud.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +kwilliams@icloud.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat mcrawfor@yahoo.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Dog;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat webteam@outlook.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat leakin@hotmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat ebassi@optonline.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -njpayne@msn.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +njpayne@msn.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat konst@live.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat godeke@me.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -godeke@yahoo.ca;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +godeke@yahoo.ca;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat glenz@gmail.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat fallorn@comcast.net;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat nacho@comcast.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -dkeeler@hotmail.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +dkeeler@hotmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat adhere@live.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat gfody@gmail.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat codex@verizon.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat @@ -444,37 +444,37 @@ miami@msn.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Cat; kewley@icloud.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat dkeeler@outlook.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat galbra@gmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -mastinfo@yahoo.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -kempsonc@sbcglobal.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +mastinfo@yahoo.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +kempsonc@sbcglobal.net;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat andale@mac.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Dog;Cat;Falcon;Cat;Cat;Cat;Cat;Cat airship@sbcglobal.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Dog;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat xtang@live.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat jhardin@yahoo.ca;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat frederic@sbcglobal.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -matsn@yahoo.ca;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +matsn@yahoo.ca;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Cat;Cat pereinar@optonline.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat salesgeek@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat tezbo@aol.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat keijser@icloud.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat chaki@yahoo.ca;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -wetter@msn.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +wetter@msn.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat delpino@icloud.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -thassine@att.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +thassine@att.net;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat hoangle@hotmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat bester@gmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat jdhedden@yahoo.ca;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Falcon;Dog;Falcon;Cat;Cat;Cat;Cat;Cat killmenow@msn.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Dog;Cat;Falcon;Cat;Cat;Cat;Cat;Cat retoh@mac.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -goresky@yahoo.ca;##null##;##null##;##null##;True;False;True;0.0;3.1415;3;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -microfab@att.net;##null##;##null##;##null##;True;False;True;0.0;3.1415;1;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -pfitza@aol.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +goresky@yahoo.ca;##null##;##null##;##null##;True;False;True;0;3.1415;3;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +microfab@att.net;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +pfitza@aol.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat onestab@hotmail.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat aracne@me.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat sherzodr@yahoo.ca;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat feamster@verizon.net;##null##;##null##;##null##;True;False;True;3.1415;3.1415;4;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat hyper@yahoo.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat jmgomez@me.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;2;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -fwitness@outlook.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +fwitness@outlook.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat storerm@comcast.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat improv@yahoo.ca;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat arnold@comcast.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat @@ -484,11 +484,11 @@ rfisher@verizon.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4; dwsauder@icloud.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat alastair@gmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Horse;Horse;Falcon;Cat;Cat;Cat;Cat;Cat multiplx@hotmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -caidaperl@icloud.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +caidaperl@icloud.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat nacho@yahoo.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat keutzer@icloud.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;1;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat lbaxter@icloud.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -hachi@live.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +hachi@live.com;##null##;##null##;##null##;True;False;True;0;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat mfburgo@msn.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat gfody@yahoo.ca;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Horse;Cat;Falcon;Cat;Cat;Cat;Cat;Cat jaxweb@yahoo.ca;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat @@ -500,25 +500,25 @@ lstein@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3; conteb@msn.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat wildixon@att.net;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat oechslin@hotmail.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;2;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -metzzo@msn.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +metzzo@msn.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat kosact@live.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -kodeman@optonline.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +kodeman@optonline.net;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat ebassi@hotmail.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat pgolle@optonline.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat jdhildeb@mac.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat garyjb@optonline.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat gslondon@me.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -maratb@msn.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -marnanel@optonline.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +maratb@msn.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +marnanel@optonline.net;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat rgiersig@live.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat gozer@msn.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat valdez@hotmail.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;4;1;Horse;Dog;Falcon;Cat;Cat;Cat;Cat;Cat mnemonic@yahoo.ca;##null##;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat paina@outlook.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -syncnine@aol.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +syncnine@aol.com;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat melnik@gmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Horse;Horse;Falcon;Cat;Cat;Cat;Cat;Cat jaesenj@sbcglobal.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -sekiya@me.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +sekiya@me.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat tbusch@aol.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat whimsy@gmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat firstpr@aol.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat @@ -529,22 +529,22 @@ bcevc@gmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;C sethbrown@me.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat mcmillan@aol.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat raines@optonline.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -psharpe@comcast.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -bachmann@gmail.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +psharpe@comcast.net;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +bachmann@gmail.com;##null##;##null##;##null##;True;False;True;0;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat leslie@att.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat hager@att.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -ismail@mac.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Cat;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +ismail@mac.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Cat;Dog;Falcon;Cat;Cat;Cat;Cat;Cat nacho@aol.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat kohlis@yahoo.ca;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat lahvak@hotmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Dog;Falcon;Cat;Cat;Cat;Cat;Cat gozer@mac.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat willg@icloud.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat zavadsky@yahoo.ca;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -steve@me.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +steve@me.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat ccohen@icloud.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat msusa@mac.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat jsmith@att.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -jshearer@outlook.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +jshearer@outlook.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat pgottsch@hotmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat majordick@gmail.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat sjava@icloud.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat @@ -556,32 +556,32 @@ portscan@sbcglobal.net;##null##;##null##;##null##;False;False;True;1.61803;3.141 morain@comcast.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat gozer@mac.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat akoblin@icloud.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -mhassel@comcast.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +mhassel@comcast.net;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat spadkins@sbcglobal.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat rohitm@yahoo.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat mwandel@yahoo.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;4;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat warrior@me.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -jipsen@aol.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;3;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +jipsen@aol.com;##null##;##null##;##null##;True;False;True;0;3.1415;3;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat bancboy@mac.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat corrada@yahoo.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat wojciech@gmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat marcs@verizon.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -atmarks@me.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;1;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +atmarks@me.com;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat quinn@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -dkeeler@aol.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -pizza@yahoo.ca;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +dkeeler@aol.com;##null##;##null##;##null##;True;False;True;0;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +pizza@yahoo.ca;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat empathy@mac.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Horse;Dog;Falcon;Cat;Cat;Cat;Cat;Cat dmouse@aol.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;4;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -dinther@comcast.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +dinther@comcast.net;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat pappp@aol.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat dougj@outlook.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat cfhsoft@msn.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat maratb@sbcglobal.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat stewwy@verizon.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Dog;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -sravani@hotmail.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +sravani@hotmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat tmaek@msn.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat juliano@yahoo.ca;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -mcsporran@optonline.net;##null##;##null##;##null##;True;False;True;0.0;3.1415;2;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +mcsporran@optonline.net;##null##;##null##;##null##;True;False;True;0;3.1415;2;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat mgemmons@yahoo.ca;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat seasweb@att.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat lushe@yahoo.ca;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat @@ -592,34 +592,34 @@ eegsa@msn.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Cat; rhavyn@hotmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat cremonini@me.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat boftx@me.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Dog;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -smartfart@outlook.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -uncled@outlook.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +smartfart@outlook.com;##null##;##null##;##null##;True;False;True;0;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +uncled@outlook.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat quantaman@aol.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat heidrich@live.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat moinefou@yahoo.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -ilial@mac.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +ilial@mac.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat fraser@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat -csilvers@me.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -csilvers@mac.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -kalpol@sbcglobal.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +csilvers@me.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +csilvers@mac.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +kalpol@sbcglobal.net;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat punkis@yahoo.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -nacho@msn.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +nacho@msn.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat mcsporran@sbcglobal.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat jaarnial@hotmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat gboss@optonline.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Dog;Cat;Falcon;Cat;Cat;Cat;Cat;Cat henkp@msn.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat philb@mac.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -hllam@yahoo.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +hllam@yahoo.com;##null##;##null##;##null##;True;False;True;0;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat roamer@yahoo.ca;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat geekgrl@comcast.net;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat cantu@outlook.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;2;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat firstpr@outlook.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;1;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat hmbrand@aol.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat arandal@icloud.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -jaarnial@live.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Horse;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +jaarnial@live.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Horse;Dog;Falcon;Cat;Cat;Cat;Cat;Cat hoyer@me.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat mmccool@att.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -smcnabb@att.net;##null##;##null##;##null##;True;False;True;0.0;3.1415;4;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +smcnabb@att.net;##null##;##null##;##null##;True;False;True;0;3.1415;4;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat pakaste@yahoo.ca;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat panolex@mac.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;4;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat hikoza@att.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat @@ -633,64 +633,64 @@ hstiles@comcast.net;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1 lushe@icloud.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat devphil@hotmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat dowdy@comcast.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -arachne@verizon.net;##null##;##null##;##null##;True;False;True;0.0;3.1415;3;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +arachne@verizon.net;##null##;##null##;##null##;True;False;True;0;3.1415;3;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat donev@icloud.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat bowmanbs@hotmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat roesch@mac.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat lridener@aol.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat hmbrand@comcast.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat sopwith@hotmail.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -vsprintf@msn.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +vsprintf@msn.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat dwsauder@icloud.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -symbolic@aol.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;2;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +symbolic@aol.com;##null##;##null##;##null##;True;False;True;0;3.1415;2;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat gbacon@live.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;2;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -hillct@verizon.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -earmstro@att.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +hillct@verizon.net;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +earmstro@att.net;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat studyabr@outlook.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;4;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat shawnce@yahoo.ca;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat boser@yahoo.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat gknauss@yahoo.ca;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat marcs@att.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -bruck@icloud.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +bruck@icloud.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Cat;Cat comdig@comcast.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Cat;Cat floxy@yahoo.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;3;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat tmccarth@icloud.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat darin@me.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -mcraigw@msn.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -fhirsch@outlook.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +mcraigw@msn.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +fhirsch@outlook.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat unreal@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat crypt@comcast.net;##null##;##null##;##null##;True;False;True;1.61803;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat pakaste@sbcglobal.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat denism@att.net;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat staffelb@aol.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -jonas@outlook.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +jonas@outlook.com;##null##;##null##;##null##;True;False;True;0;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat staikos@live.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;2;1;Horse;Cat;Falcon;Cat;Cat;Cat;Cat;Cat mfburgo@me.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -psichel@sbcglobal.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +psichel@sbcglobal.net;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat brainless@live.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat jmgomez@comcast.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat wsnyder@icloud.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -carcus@yahoo.ca;##null##;##null##;##null##;True;False;True;0.0;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +carcus@yahoo.ca;##null##;##null##;##null##;True;False;True;0;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat dmouse@outlook.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat graham@mac.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat murdocj@comcast.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat reziac@att.net;##null##;##null##;##null##;True;False;True;1.61803;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -caronni@sbcglobal.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +caronni@sbcglobal.net;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat hoyer@verizon.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -amcuri@verizon.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +amcuri@verizon.net;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat mstrout@live.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -osrin@verizon.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Falcon;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +osrin@verizon.net;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Dog;Falcon;Cat;Cat;Cat;Cat;Cat geeber@hotmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat konit@aol.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat mxiao@att.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -ryanshaw@hotmail.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +ryanshaw@hotmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat bowmanbs@aol.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -yamla@hotmail.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +yamla@hotmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat ardagna@sbcglobal.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -darin@outlook.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -jmorris@me.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;3;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -valdez@att.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -haddawy@msn.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +darin@outlook.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +jmorris@me.com;##null##;##null##;##null##;True;False;True;0;3.1415;3;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +valdez@att.net;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +haddawy@msn.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat biglou@sbcglobal.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat pplinux@icloud.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat afeldspar@optonline.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat @@ -698,10 +698,10 @@ campbell@optonline.net;##null##;##null##;##null##;True;False;True;1.61803;3.1415 akoblin@outlook.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;4;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat nwiger@yahoo.ca;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat stinson@sbcglobal.net;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Dog;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -daveed@me.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +daveed@me.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat arachne@outlook.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat augusto@live.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -xtang@me.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +xtang@me.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat itstatus@outlook.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat ebassi@optonline.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat kspiteri@yahoo.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat @@ -714,69 +714,69 @@ sisyphus@verizon.net;##null##;##null##;##null##;True;False;True;2.7182;3.1415;4; jandrese@live.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat jamuir@comcast.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Dog;Falcon;Cat;Cat;Cat;Cat;Cat dobey@yahoo.ca;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -rande@live.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;4;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +rande@live.com;##null##;##null##;##null##;True;False;True;0;3.1415;4;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat dkasak@yahoo.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat greear@msn.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat thaljef@verizon.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -sjmuir@gmail.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Dog;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +sjmuir@gmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Dog;Cat;Falcon;Cat;Cat;Cat;Cat;Cat joehall@sbcglobal.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat kronvold@optonline.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat jmorris@outlook.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;4;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat ajohnson@yahoo.ca;##null##;##null##;##null##;True;False;True;1.61803;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat russotto@mac.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat pgolle@msn.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat -mrdvt@aol.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -starstuff@icloud.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +mrdvt@aol.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +starstuff@icloud.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat jesse@live.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat luebke@outlook.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat emmanuel@optonline.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -imightb@msn.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;4;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +imightb@msn.com;##null##;##null##;##null##;True;False;True;0;3.1415;4;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat wbarker@sbcglobal.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat luvirini@hotmail.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Falcon;Dog;Falcon;Cat;Cat;Cat;Cat;Cat ylchang@yahoo.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -elflord@icloud.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;3;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +elflord@icloud.com;##null##;##null##;##null##;True;False;True;0;3.1415;3;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat scottzed@yahoo.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -mcraigw@yahoo.ca;##null##;##null##;##null##;True;False;True;0.0;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +mcraigw@yahoo.ca;##null##;##null##;##null##;True;False;True;0;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat nacho@icloud.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat rwelty@yahoo.ca;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat subir@aol.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat tbusch@yahoo.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -rupak@yahoo.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +rupak@yahoo.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat giafly@aol.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -british@outlook.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +british@outlook.com;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat hllam@icloud.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat fatelk@att.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat parsimony@verizon.net;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat tbeck@yahoo.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat citizenl@optonline.net;##null##;##null##;##null##;True;False;True;3.1415;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat jimxugle@aol.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -starstuff@sbcglobal.net;##null##;##null##;##null##;True;False;True;0.0;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -bader@icloud.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +starstuff@sbcglobal.net;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +bader@icloud.com;##null##;##null##;##null##;True;False;True;0;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat starstuff@comcast.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -satishr@yahoo.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;1;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +satishr@yahoo.com;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat ilikered@gmail.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -bader@mac.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -yruan@msn.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +bader@mac.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +yruan@msn.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat skoch@outlook.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat bader@att.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat smallpaul@sbcglobal.net;##null##;##null##;##null##;True;False;True;1.61803;3.1415;3;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -stern@verizon.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +stern@verizon.net;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat feamster@outlook.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat mcnihil@sbcglobal.net;##null##;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -firstpr@msn.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;2;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -bmorrow@yahoo.ca;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +firstpr@msn.com;##null##;##null##;##null##;True;False;True;0;3.1415;2;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +bmorrow@yahoo.ca;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat laird@att.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -ingolfke@msn.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -irving@mac.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +ingolfke@msn.com;##null##;##null##;##null##;True;False;True;0;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +irving@mac.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat monopole@me.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Horse;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -keiji@msn.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -wortmanj@gmail.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +keiji@msn.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +wortmanj@gmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat keijser@me.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat aschmitz@yahoo.ca;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -tangsh@mac.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;1;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +tangsh@mac.com;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat jdray@aol.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -kewley@yahoo.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -policies@verizon.net;##null##;##null##;##null##;True;False;True;0.0;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +kewley@yahoo.com;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +policies@verizon.net;##null##;##null##;##null##;True;False;True;0;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat wayward@outlook.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat errxn@icloud.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat fglock@icloud.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat @@ -789,14 +789,14 @@ dmiller@hotmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2; choset@live.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat dbrobins@aol.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat pizza@optonline.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -noahb@att.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +noahb@att.net;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat jespley@yahoo.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -jfriedl@hotmail.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +jfriedl@hotmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat arebenti@sbcglobal.net;##null##;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat eidac@yahoo.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Horse;Horse;Falcon;Cat;Cat;Cat;Cat;Cat jipsen@yahoo.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Horse;Horse;Falcon;Cat;Cat;Cat;Cat;Cat malin@me.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -jramio@optonline.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +jramio@optonline.net;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat jsnover@outlook.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat martyloo@gmail.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat danny@yahoo.ca;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat @@ -816,37 +816,37 @@ knorr@hotmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1; kawasaki@aol.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat ducasse@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat satishr@icloud.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -drewf@comcast.net;##null##;##null##;##null##;True;False;True;0.0;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +drewf@comcast.net;##null##;##null##;##null##;True;False;True;0;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat martyloo@yahoo.ca;##null##;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat lstein@live.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat nighthawk@me.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat debest@sbcglobal.net;##null##;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -cyrus@yahoo.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +cyrus@yahoo.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat dogdude@att.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat yruan@optonline.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat bmidd@live.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat policies@att.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -treit@att.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +treit@att.net;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat animats@msn.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat kawasaki@sbcglobal.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -jramio@yahoo.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;4;1;Horse;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +jramio@yahoo.com;##null##;##null##;##null##;True;False;True;0;3.1415;4;1;Horse;Dog;Falcon;Cat;Cat;Cat;Cat;Cat josephw@me.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -rgarcia@me.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;1;1;Horse;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +rgarcia@me.com;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Horse;Dog;Falcon;Cat;Cat;Cat;Cat;Cat ryanvm@gmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat rnewman@me.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Horse;Horse;Falcon;Cat;Cat;Cat;Cat;Cat yangyan@mac.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat tubesteak@optonline.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat munjal@sbcglobal.net;##null##;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat pgolle@live.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;2;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -milton@icloud.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +milton@icloud.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat harryh@live.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -howler@yahoo.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +howler@yahoo.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat drewf@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat cantu@optonline.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat leslie@optonline.net;##null##;##null##;##null##;True;False;True;3.1415;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat mfleming@sbcglobal.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat nelson@att.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -valdez@yahoo.ca;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +valdez@yahoo.ca;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat jsmith@msn.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat djpig@mac.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat bader@hotmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Horse;Cat;Falcon;Cat;Cat;Cat;Cat;Cat @@ -855,24 +855,24 @@ dawnsong@msn.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1; mcrawfor@optonline.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat pthomsen@icloud.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat raides@mac.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -ahuillet@icloud.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -kostas@aol.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -lridener@att.net;##null##;##null##;##null##;True;False;True;0.0;3.1415;3;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +ahuillet@icloud.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +kostas@aol.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +lridener@att.net;##null##;##null##;##null##;True;False;True;0;3.1415;3;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat maneesh@outlook.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat sartak@yahoo.ca;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -rohitm@comcast.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +rohitm@comcast.net;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat heidrich@mac.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat koudas@comcast.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat okroeger@me.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -cgcra@me.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +cgcra@me.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat janusfury@yahoo.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat seurat@comcast.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat dhrakar@mac.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat delpino@icloud.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat bebing@msn.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat keiji@gmail.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -okroeger@hotmail.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;1;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -gward@yahoo.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +okroeger@hotmail.com;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +gward@yahoo.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat jusdisgi@att.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat sakusha@comcast.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat rande@sbcglobal.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat @@ -882,14 +882,14 @@ marcs@yahoo.ca;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Hor juerd@mac.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat sethbrown@comcast.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat kdawson@verizon.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -mfburgo@aol.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +mfburgo@aol.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat crandall@hotmail.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat schwaang@msn.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -mrsam@icloud.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +mrsam@icloud.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat szymansk@sbcglobal.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Dog;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat epeeist@att.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat miyop@sbcglobal.net;##null##;##null##;##null##;True;False;True;3.1415;3.1415;1;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -wainwrig@me.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +wainwrig@me.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat pereinar@icloud.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;2;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat killmenow@mac.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat dsowsy@mac.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat @@ -899,8 +899,8 @@ mkearl@hotmail.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1; jandrese@live.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat openldap@msn.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;3;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat world@hotmail.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -adamk@att.net;##null##;##null##;##null##;True;False;True;0.0;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -pdbaby@att.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +adamk@att.net;##null##;##null##;##null##;True;False;True;0;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +pdbaby@att.net;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat hellfire@comcast.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat firstpr@optonline.net;##null##;##null##;##null##;True;False;True;3.1415;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat kenja@optonline.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat @@ -908,14 +908,14 @@ leslie@gmail.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;4;1;F bogjobber@optonline.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat hauma@verizon.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Dog;Cat;Falcon;Cat;Cat;Cat;Cat;Cat hoangle@mac.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -nimaclea@msn.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +nimaclea@msn.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat fraterk@icloud.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat ninenine@icloud.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat dogdude@att.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -campware@att.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +campware@att.net;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat amimojo@comcast.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -karasik@hotmail.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -yenya@hotmail.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +karasik@hotmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +yenya@hotmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat stevelim@gmail.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat dvdotnet@att.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat bonmots@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat @@ -925,8 +925,8 @@ alfred@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1; multiplx@optonline.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat tjensen@optonline.net;##null##;##null##;##null##;True;False;True;1.61803;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat dmath@yahoo.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -kostas@yahoo.ca;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat -carmena@gmail.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +kostas@yahoo.ca;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +carmena@gmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat terjesa@yahoo.ca;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat tjensen@sbcglobal.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat schwaang@aol.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat @@ -943,14 +943,14 @@ lpalmer@att.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Ca dgatwood@aol.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat jtorkbob@att.net;##null##;##null##;##null##;True;False;True;1.61803;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat rfoley@comcast.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Dog;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -andale@comcast.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +andale@comcast.net;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat mlewan@yahoo.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -ianbuck@yahoo.ca;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -syrinx@live.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +ianbuck@yahoo.ca;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +syrinx@live.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat imightb@live.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat gozer@icloud.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -gozer@outlook.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -shawnce@gmail.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +gozer@outlook.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +shawnce@gmail.com;##null##;##null##;##null##;True;False;True;0;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat lauronen@att.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat raines@gmail.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Cat;Cat jfriedl@icloud.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat @@ -960,25 +960,25 @@ thurston@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415; flaviog@aol.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat mnemonic@me.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat privcan@gmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -mobileip@verizon.net;##null##;##null##;##null##;True;False;True;0.0;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +mobileip@verizon.net;##null##;##null##;##null##;True;False;True;0;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat gbacon@yahoo.ca;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -caronni@optonline.net;##null##;##null##;##null##;True;False;True;0.0;3.1415;1;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +caronni@optonline.net;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat tbeck@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat keijser@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat scotfl@verizon.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat ryanshaw@sbcglobal.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -eimear@att.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +eimear@att.net;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat noticias@comcast.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -leocharre@yahoo.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +leocharre@yahoo.com;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat killmenow@yahoo.ca;##null##;##null##;##null##;True;False;True;3.1415;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat petersen@gmail.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat bdthomas@yahoo.ca;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat mavilar@msn.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat isaacson@msn.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -miyop@msn.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +miyop@msn.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat bwcarty@hotmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat warrior@mac.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat -magusnet@hotmail.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +magusnet@hotmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat crowemojo@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat crypt@optonline.net;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Cat;Dog;Falcon;Cat;Cat;Cat;Cat;Cat kempsonc@mac.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat @@ -987,27 +987,27 @@ noahb@yahoo.ca;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Fal symbolic@mac.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat wsnyder@yahoo.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat pdbaby@yahoo.ca;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -jfriedl@yahoo.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +jfriedl@yahoo.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat openldap@gmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat jwarren@optonline.net;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat dsugal@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat kayvonf@aol.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -nasarius@mac.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +nasarius@mac.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat bolow@mac.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat tbmaddux@hotmail.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat maradine@aol.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat breegster@gmail.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat breegster@sbcglobal.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat openldap@gmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -jshirley@gmail.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +jshirley@gmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat tfinniga@msn.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat delpino@mac.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat stecoop@live.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat jnolan@comcast.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -jhardin@yahoo.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;1;1;Dog;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +jhardin@yahoo.com;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Dog;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat teverett@sbcglobal.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Horse;Horse;Falcon;Cat;Cat;Cat;Cat;Cat wsnyder@comcast.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat treeves@msn.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -garland@outlook.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +garland@outlook.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat ullman@comcast.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -sumdumass@outlook.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat \ No newline at end of file +sumdumass@outlook.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat diff --git a/tests/data/testmatrix_and_or.csv b/tests/data/testmatrix_and_or.csv new file mode 100644 index 0000000..5a149f4 --- /dev/null +++ b/tests/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/tests/data/testmatrix_comparators_v6.csv b/tests/data/testmatrix_comparators_v6.csv new file mode 100644 index 0000000..d53efb5 --- /dev/null +++ b/tests/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/tests/testmatrix_number.csv b/tests/data/testmatrix_number.csv similarity index 96% rename from tests/testmatrix_number.csv rename to tests/data/testmatrix_number.csv index b2b603d..56a5350 100644 --- a/tests/testmatrix_number.csv +++ b/tests/data/testmatrix_number.csv @@ -23,4 +23,4 @@ id15;;;4;<>4.2;<>5 id16;;;4.0;<>4.2;<>5 id17;;;4,0;<>4.2;<>5 id18;;;4.2;80%;<>5 -id19;;;4,2;20%;<>5 \ No newline at end of file +id19;;;4,2;20%;<>5 diff --git a/tests/data/testmatrix_prerequisite_flag.csv b/tests/data/testmatrix_prerequisite_flag.csv new file mode 100644 index 0000000..dcf68f4 --- /dev/null +++ b/tests/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/tests/data/testmatrix_segments.csv b/tests/data/testmatrix_segments.csv new file mode 100644 index 0000000..b59ba3a --- /dev/null +++ b/tests/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/tests/data/testmatrix_segments_old.csv b/tests/data/testmatrix_segments_old.csv new file mode 100644 index 0000000..9fc605e --- /dev/null +++ b/tests/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/tests/testmatrix_semantic.csv b/tests/data/testmatrix_semantic.csv similarity index 99% rename from tests/testmatrix_semantic.csv rename to tests/data/testmatrix_semantic.csv index 2ac8df7..a5a7046 100644 --- a/tests/testmatrix_semantic.csv +++ b/tests/data/testmatrix_semantic.csv @@ -33,4 +33,4 @@ id27;;;3.0.1;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, id28;;;3.1.0;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );80%;>2.0.0 id28;;;3.1.1;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );80%;>2.0.0 id29;;;5.0.0;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );80%;>2.0.0 -id30;;;5.99.999;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );20%;>2.0.0 \ No newline at end of file +id30;;;5.99.999;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );20%;>2.0.0 diff --git a/tests/testmatrix_semantic_2.csv b/tests/data/testmatrix_semantic_2.csv similarity index 100% rename from tests/testmatrix_semantic_2.csv rename to tests/data/testmatrix_semantic_2.csv diff --git a/tests/testmatrix_sensitive.csv b/tests/data/testmatrix_sensitive.csv similarity index 100% rename from tests/testmatrix_sensitive.csv rename to tests/data/testmatrix_sensitive.csv diff --git a/tests/data/testmatrix_unicode.csv b/tests/data/testmatrix_unicode.csv new file mode 100644 index 0000000..e5b01de --- /dev/null +++ b/tests/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/tests/testmatrix_variationId.csv b/tests/data/testmatrix_variationId.csv similarity index 75% rename from tests/testmatrix_variationId.csv rename to tests/data/testmatrix_variationId.csv index ebe8094..8f76cd4 100644 --- a/tests/testmatrix_variationId.csv +++ b/tests/data/testmatrix_variationId.csv @@ -1,8 +1,8 @@ Identifier;Email;Country;Custom1;boolean;decimal;text;whole -##null##;;;;a0e56eda;63612d39;3f05be89;cf2e9162 -a@configcat.com;a@configcat.com;Hungary;admin;67787ae4;8f9559cf;9bdc6a1f;ab30533b -b@configcat.com;b@configcat.com;Hungary;admin;67787ae4;8f9559cf;9bdc6a1f;ab30533b -a@test.com;a@test.com;Hungary;admin;67787ae4;d66c5781;65310deb;ec14f6a9 -b@test.com;b@test.com;Hungary;admin;a0e56eda;d66c5781;65310deb;ec14f6a9 -cliffordj@aol.com;cliffordj@aol.com;Hungary;admin;67787ae4;8155ad7b;cf19e913;ec14f6a9 -bryanw@verizon.net;bryanw@verizon.net;Hungary;;a0e56eda;d0dbc27f;30ba32b9;61a5a033 +##null##;;;;a0e56eda;63612d39;3f05be89;cf2e9162; +a@configcat.com;a@configcat.com;Hungary;admin;67787ae4;8f9559cf;9bdc6a1f;ab30533b; +b@configcat.com;b@configcat.com;Hungary;admin;67787ae4;8f9559cf;9bdc6a1f;ab30533b; +a@test.com;a@test.com;Hungary;admin;67787ae4;d66c5781;65310deb;ec14f6a9; +b@test.com;b@test.com;Hungary;admin;a0e56eda;d66c5781;65310deb;ec14f6a9; +cliffordj@aol.com;cliffordj@aol.com;Hungary;admin;67787ae4;8155ad7b;cf19e913;ec14f6a9; +bryanw@verizon.net;bryanw@verizon.net;Hungary;;a0e56eda;d0dbc27f;30ba32b9;61a5a033; diff --git a/tests/test-rules.json b/tests/test-rules.json deleted file mode 100644 index f39953b..0000000 --- a/tests/test-rules.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "f": { - "rolloutFeature": { - "v": false, - "r": [ - { - "o": 0, - "a": "Identifier", - "t": 2, - "c": "@example.com", - "v": true - } - ] - } - } -} \ No newline at end of file diff --git a/tests/test.json b/tests/test.json deleted file mode 100644 index d547507..0000000 --- a/tests/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