From 136f7ae725768abfe1413b7e8808c7af71bccd53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=B3th=20D=C3=A1niel?= Date: Tue, 2 Apr 2024 12:25:47 +0200 Subject: [PATCH] Config v6 (#18) * SDKKey validation updated to handle proxy and new key format * Comparators rework Removed:IS ONE OF, IS NOT ONE OF Changed: CONTAINS, DOES_NOT_CONTAIN Added: DATE_BEFORE, DATE_AFTER, HASHED_EQUALS, HASHED_NOT_EQUALS, HASHED_STARTS_WITH, HASHED_ENDS_WITH, HASHED_ARRAY_CONTAINS, HASHED_ARRAY_NOT_CONTAINS * Fix comparators names * Fix comparator names and add new comparator * v6 and models added * Implement evaluatePrerequisiteFlagCondition and added v6 tests * Fix comparator and other errors * Added segments impl and test * Rename ComparisonCondition to UserCondition * Added logs to new code * Evaluation logger * Evaluation logs and tests finished * Logs NumberFormat updated and DateUTC fixed * Remove TODOs * EvaluateLogger depends on LogLevel * v5 and v6 sdkKey updates * EvaluationLoggerTurnOffTest updated with LogLevel checks * Added attributeValueFrom user helper methods. (no date yet) * Format code * Fix matrix test v5vsV6. Add remoteJson to tests * Detekt fixes * Ktlint fixes * Add docs * Fix list truncation * Date double value format fix. Cannot convert to Long because it's not milliseconds * Fix JS test errors. Lint and detekt problems. * Added text comparators. Updated segments evaluation. Updated tests. * Unicode test and fixes added * Small fix and reduce logged info in tests * Evaluator warning fixes * Format * Fix failing JS tests * Remove println * Added tests and small fixes. CCUser handle Any values * JS number settingtype added to handle JS number conversion. * Fix JS Evaluation fails because of cache message * Update 1103 error message * Fix circular dependency missing remove. * Rename Comparator to USerComparator. Fix user attribute override. Set evaluateConditions default result to true. Fix variationIDTests json. Fix getKeyAndValue method to handle targetRule percentage options as well. * Fix getKeyAndValue targeting rule if. Add exception to evaluatePercentageOptions if sum is not 100%. * Move configSalt validation from deserialization. * update processHashedStartEndsWithCompare handle sliced String * Update trim and refactor process methods. * Accept NaN * Added extra segment and prerequisite tests * Error message fixes * SDK key validation LOCAL_ONLY fix * Code format * Fix httpEngine in test * klint fix * Added missing PrerequisiteFlag Override Test * Add trim test Fix user value version trim * Add trim test Fix user value version trim * Fix user toString use simple json. Fix comparatorsTests. * Fix Array and List convert to String to use json. * Add specialCharacter test * Refactor LogHelper * Platform based double format WIP * Set stringbuilder lineseparator as default * Lint fixes * Fixes after merge. Exclude native format test. * Change DataSource setting to config. Fix override test. Fix missing segment and configSalt mapping. * JS platform send agent and etag info in the request query params. * Remove expected NumberFormatter class. Now just the doubleToString method expected and implemented. * Fix date conversions * Fix log double format. Now only the native platform is different from the expected. * NSNumberFormatter added (not tested) * lint fix * fix imports * Added darwinTest module * Try to fix darwin number formatter * Type validation updated and some test added * lint fix * Try to fix formatter * Update NumberFormatter.kt * darwin test fix * darwin test fix * darwin test fix * Fix test after merge. Move EvaluateLogger to a new file. * Value type and setting type validation fixed. getAnyValue and getAnyValueDetails check allowed types. Missing hooks and error logging added. getAnyValue and getAnyValueDetails allow null defaultValue. Fix tests based on changes and add some type validation tests. * Fix tests * Fix darwin test * Update version to 3.0.0 * Fixes on model based on code review * Fixes based on code review * Fixes based on code review * Fix based on code review * Refactor getValue/getAnyValue and getValueDetails/getAnyValueDetails to have a consistent behavior with other SDKs (#27) * Hashed length trim added * Fixes based on review * Add analysis to PR checks * Detekt and lint fixes * SemVer fix. Lint fix * Added when fix * Add tests * klint fix * detekt fix * Add more test * Fix testGetValueDetailsValidTypes * Fix test error * Moved Utils method to internal object * Detekt fix * Lint fix * Rename Utils file to Constants and internal object Utils to Helpers * Added FlagValueSerializer tests * Update fetcher param test to check http/2 headers. * Added getKeyAndValue tests --------- Co-authored-by: Peter Csajtai Co-authored-by: adams85 <31276480+adams85@users.noreply.github.com> --- .github/workflows/test.yml | 86 + build.gradle.kts | 15 +- detekt.yml | 10 +- gradle.properties | 6 +- gradle/wrapper/gradle-wrapper.jar | Bin 59821 -> 60756 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 6 + gradlew.bat | 14 +- src/androidMain/kotlin/com/configcat/Cache.kt | 3 +- .../kotlin/com/configcat/NumberFormatter.kt | 35 + .../com/configcat/fetch/HttpRequestBuilder.kt | 10 + .../configcat/override/SettingConverter.kt | 7 + src/commonMain/kotlin/com/configcat/Config.kt | 194 -- .../kotlin/com/configcat/ConfigCache.kt | 3 +- .../kotlin/com/configcat/ConfigCatClient.kt | 380 ++- .../kotlin/com/configcat/ConfigCatUser.kt | 63 +- .../kotlin/com/configcat/ConfigService.kt | 11 +- .../kotlin/com/configcat/Constants.kt | 123 + .../kotlin/com/configcat/DateTimeUtils.kt | 8 + .../kotlin/com/configcat/EvaluateLogger.kt | 346 +++ .../kotlin/com/configcat/EvaluationDetails.kt | 27 +- .../kotlin/com/configcat/Evaluator.kt | 1396 ++++++++--- src/commonMain/kotlin/com/configcat/Hooks.kt | 5 +- src/commonMain/kotlin/com/configcat/Utils.kt | 26 - .../com/configcat/fetch/ConfigFetcher.kt | 45 +- .../com/configcat/fetch/FetchResponse.kt | 2 +- .../com/configcat/log/ConfigCatLogMessages.kt | 122 +- .../kotlin/com/configcat/model/Condition.kt | 17 + .../com/configcat/model/ConditionAccessor.kt | 10 + .../kotlin/com/configcat/model/Config.kt | 32 + .../kotlin/com/configcat/model/Entry.kt | 45 + .../com/configcat/model/PercentageOption.kt | 28 + .../kotlin/com/configcat/model/Preferences.kt | 20 + .../model/PrerequisiteFlagCondition.kt | 29 + .../kotlin/com/configcat/model/Segment.kt | 24 + .../com/configcat/model/SegmentCondition.kt | 23 + .../kotlin/com/configcat/model/Setting.kt | 50 + .../kotlin/com/configcat/model/SettingType.kt | 14 + .../com/configcat/model/SettingValue.kt | 74 + .../com/configcat/model/TargetingRule.kt | 41 + .../com/configcat/model/UserCondition.kt | 47 + .../com/configcat/override/DataSource.kt | 46 +- .../kotlin/com/configcat/CommonUtilsTests.kt | 138 ++ .../com/configcat/ConfigCatClientTests.kt | 709 +++++- .../com/configcat/ConfigFetcherTests.kt | 81 +- .../com/configcat/ConfigServiceTests.kt | 129 +- .../com/configcat/ConfigV2EvaluationTest.kt | 2183 +++++++++++++++++ .../com/configcat/DataGovernanceTests.kt | 2 +- .../com/configcat/EntrySerializationTests.kt | 10 +- .../com/configcat/EvaluatorTrimTests.kt | 1946 +++++++++++++++ .../kotlin/com/configcat/OverrideTests.kt | 81 +- src/commonTest/kotlin/com/configcat/Utils.kt | 96 +- .../kotlin/com/configcat/VariationIdTests.kt | 254 +- .../EvaluationLoggerTurnOffTests.kt | 662 +++++ .../evaluation/EvaluationTestLogger.kt | 53 + .../configcat/evaluation/EvaluationTests.kt | 164 ++ .../evaluation/data/AndRulesTests.kt | 463 ++++ .../evaluation/data/ComparatorsTests.kt | 1457 +++++++++++ .../data/EpochDateValidationTests.kt | 1402 +++++++++++ .../evaluation/data/ListTruncationTests.kt | 65 + .../evaluation/data/NumberValidationTests.kt | 185 ++ .../evaluation/data/OneTargetingRuleTests.kt | 648 +++++ .../data/OptionsAfterTargetingRuleTests.kt | 654 +++++ .../data/OptionsBasedOnCustomAttrTests.kt | 205 ++ .../data/OptionsBasedOnUserIdTests.kt | 625 +++++ .../data/OptionsWithinTargetingRuleTests.kt | 234 ++ .../evaluation/data/PrerequisiteFlagTests.kt | 660 +++++ .../configcat/evaluation/data/SegmentTests.kt | 387 +++ .../evaluation/data/SemverValidationTests.kt | 545 ++++ .../evaluation/data/SimpleValueTests.kt | 642 +++++ .../com/configcat/evaluation/data/TestCase.kt | 11 + .../com/configcat/evaluation/data/TestSet.kt | 8 + .../evaluation/data/TwoTargetingRulesTests.kt | 659 +++++ .../integration/RolloutMatrixTests.kt | 37 +- .../integration/matrix/AndOrMatrix.kt | 449 ++++ .../integration/matrix/ComparatorsV6Matrix.kt | 1409 +++++++++++ .../configcat/integration/matrix/Matrix.kt | 597 ++++- .../integration/matrix/NumberMatrix.kt | 165 +- .../matrix/PrerequisiteFlagMatrix.kt | 533 ++++ .../integration/matrix/SegmentMatrix.kt | 281 +++ .../integration/matrix/SegmentsOldMatrix.kt | 276 +++ .../integration/matrix/SemanticMatrix.kt | 494 +++- .../integration/matrix/SemanticMatrix2.kt | 1416 ++++++++++- .../integration/matrix/SensitiveMatrix.kt | 151 +- .../integration/matrix/UnicodeMatrix.kt | 676 +++++ .../integration/matrix/VariationIdMatrix.kt | 245 +- .../userattribute/UserAttributeConvertTest.kt | 132 + .../userattribute/data/ConvertData.kt | 8 + .../userattribute/data/DateConvertData.kt | 1412 +++++++++++ .../userattribute/data/NumberConvertData.kt | 171 ++ .../userattribute/data/SemVerConvertData.kt | 500 ++++ .../data/StringArrayConvertData.kt | 1412 +++++++++++ .../userattribute/data/StringConvertData.kt | 1412 +++++++++++ .../kotlin/com/configcat/NumberFormatter.kt | 41 + .../com/configcat/fetch/HttpRequestBuilder.kt | 10 + .../configcat/override/SettingConverter.kt | 7 + .../com/configcat/DarwinEvaluationTests.kt | 96 + .../configcat/data/DarwinComparatorsTests.kt | 1459 +++++++++++ .../data/DarwinEpochDateValidationTests.kt | 1404 +++++++++++ .../kotlin/com/configcat/NumberFormatter.kt | 10 + .../com/configcat/fetch/HttpRequestBuilder.kt | 14 + .../configcat/override/SettingConverter.kt | 26 + .../com/configcat/JSConfigV2EvaluationTest.kt | 630 +++++ .../com/configcat/JsConfigFetcherTests.kt | 83 + .../kotlin/com/configcat/NumberFormatter.kt | 35 + .../com/configcat/fetch/HttpRequestBuilder.kt | 10 + .../configcat/override/SettingConverter.kt | 7 + .../kotlin/com/configcat/NumberFormatter.kt | 9 + .../com/configcat/fetch/HttpRequestBuilder.kt | 10 + .../configcat/override/SettingConverter.kt | 7 + .../com/configcat/NativeEvaluationTests.kt | 92 + .../configcat/data/NativeComparatorsTests.kt | 1459 +++++++++++ .../data/NativeEpochDateValidationTests.kt | 1404 +++++++++++ 113 files changed, 36370 insertions(+), 1022 deletions(-) create mode 100644 src/androidMain/kotlin/com/configcat/NumberFormatter.kt create mode 100644 src/androidMain/kotlin/com/configcat/fetch/HttpRequestBuilder.kt create mode 100644 src/androidMain/kotlin/com/configcat/override/SettingConverter.kt delete mode 100644 src/commonMain/kotlin/com/configcat/Config.kt create mode 100644 src/commonMain/kotlin/com/configcat/Constants.kt create mode 100644 src/commonMain/kotlin/com/configcat/EvaluateLogger.kt delete mode 100644 src/commonMain/kotlin/com/configcat/Utils.kt create mode 100644 src/commonMain/kotlin/com/configcat/model/Condition.kt create mode 100644 src/commonMain/kotlin/com/configcat/model/ConditionAccessor.kt create mode 100644 src/commonMain/kotlin/com/configcat/model/Config.kt create mode 100644 src/commonMain/kotlin/com/configcat/model/Entry.kt create mode 100644 src/commonMain/kotlin/com/configcat/model/PercentageOption.kt create mode 100644 src/commonMain/kotlin/com/configcat/model/Preferences.kt create mode 100644 src/commonMain/kotlin/com/configcat/model/PrerequisiteFlagCondition.kt create mode 100644 src/commonMain/kotlin/com/configcat/model/Segment.kt create mode 100644 src/commonMain/kotlin/com/configcat/model/SegmentCondition.kt create mode 100644 src/commonMain/kotlin/com/configcat/model/Setting.kt create mode 100644 src/commonMain/kotlin/com/configcat/model/SettingType.kt create mode 100644 src/commonMain/kotlin/com/configcat/model/SettingValue.kt create mode 100644 src/commonMain/kotlin/com/configcat/model/TargetingRule.kt create mode 100644 src/commonMain/kotlin/com/configcat/model/UserCondition.kt create mode 100644 src/commonTest/kotlin/com/configcat/CommonUtilsTests.kt create mode 100644 src/commonTest/kotlin/com/configcat/ConfigV2EvaluationTest.kt create mode 100644 src/commonTest/kotlin/com/configcat/EvaluatorTrimTests.kt create mode 100644 src/commonTest/kotlin/com/configcat/evaluation/EvaluationLoggerTurnOffTests.kt create mode 100644 src/commonTest/kotlin/com/configcat/evaluation/EvaluationTestLogger.kt create mode 100644 src/commonTest/kotlin/com/configcat/evaluation/EvaluationTests.kt create mode 100644 src/commonTest/kotlin/com/configcat/evaluation/data/AndRulesTests.kt create mode 100644 src/commonTest/kotlin/com/configcat/evaluation/data/ComparatorsTests.kt create mode 100644 src/commonTest/kotlin/com/configcat/evaluation/data/EpochDateValidationTests.kt create mode 100644 src/commonTest/kotlin/com/configcat/evaluation/data/ListTruncationTests.kt create mode 100644 src/commonTest/kotlin/com/configcat/evaluation/data/NumberValidationTests.kt create mode 100644 src/commonTest/kotlin/com/configcat/evaluation/data/OneTargetingRuleTests.kt create mode 100644 src/commonTest/kotlin/com/configcat/evaluation/data/OptionsAfterTargetingRuleTests.kt create mode 100644 src/commonTest/kotlin/com/configcat/evaluation/data/OptionsBasedOnCustomAttrTests.kt create mode 100644 src/commonTest/kotlin/com/configcat/evaluation/data/OptionsBasedOnUserIdTests.kt create mode 100644 src/commonTest/kotlin/com/configcat/evaluation/data/OptionsWithinTargetingRuleTests.kt create mode 100644 src/commonTest/kotlin/com/configcat/evaluation/data/PrerequisiteFlagTests.kt create mode 100644 src/commonTest/kotlin/com/configcat/evaluation/data/SegmentTests.kt create mode 100644 src/commonTest/kotlin/com/configcat/evaluation/data/SemverValidationTests.kt create mode 100644 src/commonTest/kotlin/com/configcat/evaluation/data/SimpleValueTests.kt create mode 100644 src/commonTest/kotlin/com/configcat/evaluation/data/TestCase.kt create mode 100644 src/commonTest/kotlin/com/configcat/evaluation/data/TestSet.kt create mode 100644 src/commonTest/kotlin/com/configcat/evaluation/data/TwoTargetingRulesTests.kt create mode 100644 src/commonTest/kotlin/com/configcat/integration/matrix/AndOrMatrix.kt create mode 100644 src/commonTest/kotlin/com/configcat/integration/matrix/ComparatorsV6Matrix.kt create mode 100644 src/commonTest/kotlin/com/configcat/integration/matrix/PrerequisiteFlagMatrix.kt create mode 100644 src/commonTest/kotlin/com/configcat/integration/matrix/SegmentMatrix.kt create mode 100644 src/commonTest/kotlin/com/configcat/integration/matrix/SegmentsOldMatrix.kt create mode 100644 src/commonTest/kotlin/com/configcat/integration/matrix/UnicodeMatrix.kt create mode 100644 src/commonTest/kotlin/com/configcat/userattribute/UserAttributeConvertTest.kt create mode 100644 src/commonTest/kotlin/com/configcat/userattribute/data/ConvertData.kt create mode 100644 src/commonTest/kotlin/com/configcat/userattribute/data/DateConvertData.kt create mode 100644 src/commonTest/kotlin/com/configcat/userattribute/data/NumberConvertData.kt create mode 100644 src/commonTest/kotlin/com/configcat/userattribute/data/SemVerConvertData.kt create mode 100644 src/commonTest/kotlin/com/configcat/userattribute/data/StringArrayConvertData.kt create mode 100644 src/commonTest/kotlin/com/configcat/userattribute/data/StringConvertData.kt create mode 100644 src/darwinMain/kotlin/com/configcat/NumberFormatter.kt create mode 100644 src/darwinMain/kotlin/com/configcat/fetch/HttpRequestBuilder.kt create mode 100644 src/darwinMain/kotlin/com/configcat/override/SettingConverter.kt create mode 100644 src/darwinTest/kotlin/com/configcat/DarwinEvaluationTests.kt create mode 100644 src/darwinTest/kotlin/com/configcat/data/DarwinComparatorsTests.kt create mode 100644 src/darwinTest/kotlin/com/configcat/data/DarwinEpochDateValidationTests.kt create mode 100644 src/jsMain/kotlin/com/configcat/NumberFormatter.kt create mode 100644 src/jsMain/kotlin/com/configcat/fetch/HttpRequestBuilder.kt create mode 100644 src/jsMain/kotlin/com/configcat/override/SettingConverter.kt create mode 100644 src/jsTest/kotlin/com/configcat/JSConfigV2EvaluationTest.kt create mode 100644 src/jsTest/kotlin/com/configcat/JsConfigFetcherTests.kt create mode 100644 src/jvmMain/kotlin/com/configcat/NumberFormatter.kt create mode 100644 src/jvmMain/kotlin/com/configcat/fetch/HttpRequestBuilder.kt create mode 100644 src/jvmMain/kotlin/com/configcat/override/SettingConverter.kt create mode 100644 src/nativeMain/kotlin/com/configcat/NumberFormatter.kt create mode 100644 src/nativeMain/kotlin/com/configcat/fetch/HttpRequestBuilder.kt create mode 100644 src/nativeMain/kotlin/com/configcat/override/SettingConverter.kt create mode 100644 src/nativeTest/kotlin/com/configcat/NativeEvaluationTests.kt create mode 100644 src/nativeTest/kotlin/com/configcat/data/NativeComparatorsTests.kt create mode 100644 src/nativeTest/kotlin/com/configcat/data/NativeEpochDateValidationTests.kt diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e074ab07..250a0860 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -50,4 +50,90 @@ jobs: uses: ./.github/actions/cache-gradle - name: Run format check run: ./gradlew ktlintCheck --stacktrace + shell: bash + + analysis: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up JDK + uses: actions/setup-java@v4 + with: + java-version: 17 + distribution: zulu + - name: Cache Konan + uses: ./.github/actions/cache-konan + - name: Cache Gradle + uses: ./.github/actions/cache-gradle + - name: Run code analysis + run: ./gradlew detekt --stacktrace + shell: bash + - name: Upload SARIF file + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: build/reports/detekt/detekt.sarif + - name: Upload analysis report + uses: actions/upload-artifact@v4 + with: + name: analysis-report + path: build/reports/detekt + + coverage: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up JDK + uses: actions/setup-java@v4 + with: + java-version: 11 + distribution: zulu + - name: Cache Konan + uses: ./.github/actions/cache-konan + - name: Cache Gradle + uses: ./.github/actions/cache-gradle + - name: Calculate coverage + run: ./gradlew koverXmlReport --stacktrace + shell: bash + - name: Upload coverage report + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: build/reports/kover + + upload-reports: + needs: [ test, analysis, coverage, lint ] + runs-on: ubuntu-latest + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BUILD_NUMBER: ${{ github.run_number }} + steps: + - uses: actions/checkout@v4 + - name: Set up JDK + uses: actions/setup-java@v4 + with: + java-version: 17 + distribution: zulu + - name: SonarCloud cache + uses: actions/cache@v4 + with: + path: ~/.sonar/cache + key: ${{ runner.os }}-sonar + restore-keys: ${{ runner.os }}-sonar + - name: Cache Konan + uses: ./.github/actions/cache-konan + - name: Cache Gradle + uses: ./.github/actions/cache-gradle + - name: Download coverage report + uses: actions/download-artifact@v4 + with: + name: coverage-report + path: build/reports/kover + - name: Download analysis report + uses: actions/download-artifact@v4 + with: + name: analysis-report + path: build/reports/detekt + - name: Upload reports to SonarCloud + run: ./gradlew sonarqube --stacktrace shell: bash \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 87f6e4a8..d969f865 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -83,7 +83,7 @@ kotlin { nodejs { testTask { useMocha { - timeout = "10000" + timeout = "20000" } } } @@ -176,6 +176,13 @@ kotlin { } } + val darwinTest by creating { + dependsOn(commonTest) + dependencies { + implementation("io.ktor:ktor-client-darwin:$ktor_version") + } + } + val nativeMain by creating { dependsOn(commonMain) } @@ -193,7 +200,11 @@ kotlin { } configure(nativeTestSets) { - dependsOn(nativeTest) + if (this.name.isDarwin()) { + dependsOn(darwinTest) + } else { + dependsOn(nativeTest) + } } } } diff --git a/detekt.yml b/detekt.yml index 1ad7c824..eb7059d8 100644 --- a/detekt.yml +++ b/detekt.yml @@ -4,14 +4,13 @@ style: WildcardImport: active: false ReturnCount: - max: 8 + max: 9 DestructuringDeclarationWithTooManyEntries: maxDestructuringEntries: 5 - complexity: ComplexCondition: - threshold: 5 + threshold: 11 TooManyFunctions: thresholdInClasses: 22 thresholdInInterfaces: 17 @@ -19,6 +18,11 @@ complexity: threshold: 70 LongParameterList: constructorThreshold: 10 + functionThreshold: 7 + CyclomaticComplexMethod: + threshold: 17 + NestedBlockDepth: + threshold: 10 exceptions: TooGenericExceptionCaught: diff --git a/gradle.properties b/gradle.properties index 1244b504..e14bc6ee 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ group=com.configcat -version=2.1.0 +version=3.0.0 ktor_version=2.0.3 kotlinx_serialization_version=1.4.1 @@ -17,4 +17,6 @@ kotlin.native.ignoreDisabledTargets=true kotlin.mpp.stability.nowarn=true kotlin.native.binary.memoryModel=experimental -org.gradle.jvmargs=-Xmx6g \ No newline at end of file +org.gradle.jvmargs=-Xmx6g + +kotlin.ignore.tcsm.overflow=true diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 41d9927a4d4fb3f96a785543079b8df6723c946b..249e5832f090a2944b7473328c07c9755baa3196 100644 GIT binary patch delta 10197 zcmaKS1ymhDwk=#NxVyW%y9U<)A-Dv)xI0|j{UX8L-JRg>5ZnnKAh;%chM6~S-g^K4 z>eZ{yK4;gd>gwvXs=Id8Jk-J}R4pT911;+{Jp9@aiz6!p1Oz9z&_kGLA%J5%3Ih@0 zQ|U}%$)3u|G`jIfPzMVfcWs?jV2BO^*3+q2><~>3j+Z`^Z%=;19VWg0XndJ zwJ~;f4$;t6pBKaWn}UNO-wLCFHBd^1)^v%$P)fJk1PbK5<;Z1K&>k~MUod6d%@Bq9 z>(44uiaK&sdhwTTxFJvC$JDnl;f}*Q-^01T508(8{+!WyquuyB7R!d!J)8Ni0p!cV6$CHsLLy6}7C zYv_$eD;)@L)tLj0GkGpBoa727hs%wH$>EhfuFy{_8Q8@1HI%ZAjlpX$ob{=%g6`Ox zLzM!d^zy`VV1dT9U9(^}YvlTO9Bf8v^wMK37`4wFNFzW?HWDY(U(k6@tp(crHD)X5>8S-# zW1qgdaZa*Sh6i%60e1+hty}34dD%vKgb?QmQiZ=-j+isA4={V_*R$oGN#j|#ia@n6 zuZx4e2Xx?^lUwYFn2&Tmbx0qA3Z8;y+zKoeQu;~k~FZGy!FU_TFxYd!Ck;5QvMx9gj5fI2@BLNp~Ps@ zf@k<&Q2GS5Ia9?_D?v~$I%_CLA4x~eiKIZ>9w^c#r|vB?wXxZ(vXd*vH(Fd%Me8p( z=_0)k=iRh%8i`FYRF>E97uOFTBfajv{IOz(7CU zv0Gd84+o&ciHlVtY)wn6yhZTQQO*4Mvc#dxa>h}82mEKKy7arOqU$enb9sgh#E=Lq zU;_RVm{)30{bw+|056%jMVcZRGEBSJ+JZ@jH#~DvaDQm92^TyUq=bY*+AkEakpK>8 zB{)CkK48&nE5AzTqT;WysOG|!y}5fshxR8Ek(^H6i>|Fd&wu?c&Q@N9ZrJ=?ABHI! z`*z8D`w=~AJ!P-9M=T}f`;76$qZRllB&8#9WgbuO$P7lVqdX1=g*t=7z6!0AQ^ux_ z9rcfUv^t}o_l-ZE+TqvqFsA*~W<^78!k;~!i8(eS+(+@u8FxK+Q7;mHZ<1}|4m<}vh@p`t%|@eM_J(P% zI>M7C)Ir{l|J;$G_EGGEhbP4?6{sYzMqBv+x95N&YWFH6UcE@b}B?q)G*4<4mR@sy1#vPnLMK51tb#ED(8TA1nE zYfhK7bo1!R5WJF$5Y?zG21)6+_(_5oSX9sGIW;(O&S?Rh(nydNQYzKjjJ54aDJ-1F zrJ=np8LsN?%?Rt7f~3aAX!2E{`fh_pb?2(;HOB3W+I*~A>W%iY+v45+^e$cE10fA} zXPvw9=Bd+(;+!rl)pkYj0HGB}+3Z!Mr;zr%gz~c-hFMv8b2VRE2R$8V=_XE zq$3=|Yg05(fmwrJ)QK2ptB4no`Y8Dg_vK2QDc6-6sXRQ5k78-+cPi-fH}vpgs|Ive zE=m*XNVs?EWgiNI!5AcD*3QMW)R`EqT!f0e1%hERO&?AT7HWnSf5@#AR{OGuXG3Zb zCnVWg7h|61lGV3k+>L<#d>)InG>ETn1DbOHCfztqzQ_fBiaUt@q6VMy={Fe-w#~2- z0?*f|z$zgjI9>+JVICObBaK=pU}AEOd@q(8d?j7zQFD@=6t`|KmolTr2MfBI$;EGh zD%W0cA_d#V6Lb$us5yIG(|d>r-QleC4;%hEu5W9hyY zY#+ESY&v`8(&mC~?*|e5WEhC!YU2>m_}`K+q9)a(d$bsS<=YkyZGp}YA%TXw>@abA zS_poVPoN+?<6?DAuCNt&5SHV(hp56PJ})swwVFZFXM->F zc|0c8<$H_OV%DR|y7e+s$12@Ac8SUClPg8_O9sTUjpv%6Jsn5vsZCg>wL+db4c+{+ zsg<#wOuV4jeOq`veckdi-1`dz;gvL)bZeH|D*x=8UwRU5&8W1@l>3$)8WzET0%;1J zM3(X<7tKK&9~kWRI{&FmwY5Gg!b5f4kI_vSm)H1#>l6M+OiReDXC{kPy!`%Ecq-+3yZTk=<` zm)pE6xum5q0Qkd#iny0Q-S}@I0;mDhxf>sX)Oiv)FdsAMnpx%oe8OQ`m%Xeozdzx!C1rQR>m1c_}+J4x)K}k{G zo68;oGG&Ox7w^-m7{g4a7NJu-B|~M;oIH~~#`RyUNm##feZH;E?pf}nshmoiIY52n z%pc%lnU4Q#C=RUz)RU6}E_j4#)jh<&a%JyJj$Fufc#&COaxFHtl}zJUGNLBu3~_@1 zn9F^JO9);Duxo&i@>X(kbYga1i>6p1fca8FzQ0>((Lb-aPUbC*d~a03V$y;*RBY!R ziEJ2IF^FjrvO}0Uy{cMn%u<+P5U!UO>pm9#ZYL5i6|xSC+np7IH$GfXs&uI;y4as@ z&AzJh>(S2?3PKKgab3Z(`xbx(C#46XIvVcW8eG_DjT~}Yz_8PWZ`uf6^Xr=vkvL_` zqmvfgJL+Zc`;iq~iP?%@G7}~fal-zqxa0yNyHBJJ5M)9bI>7S_cg?Ya&p(I)C5Ef4 zZ>YAF6x|U=?ec?g*|f2g5Tw3PgxaM_bi_5Az9MO$;_Byw(2d}2%-|bg4ShdQ;)Z|M z4K|tFv)qx*kKGKoyh!DQY<{n&UmAChq@DJrQP>EY7g1JF(ih*D8wCVWyQ z5Jj^|-NVFSh5T0vd1>hUvPV6?=`90^_)t(L9)XOW7jeP45NyA2lzOn&QAPTl&d#6P zSv%36uaN(9i9WlpcH#}rmiP#=L0q(dfhdxvFVaOwM;pY;KvNQ9wMyUKs6{d}29DZQ z{H3&Sosr6)9Z+C>Q5)iHSW~gGoWGgK-0;k~&dyr-bA3O|3PCNzgC?UKS_B=^i8Ri^ zd_*_qI4B07Cayq|p4{`U_E_P=K`N_~{F|+-+`sCgcNxs`%X!$=(?l2aAW}0M=~COb zf19oe^iuAUuDEf)4tgv<=WRPpK@IjToNNC*#&Ykw!)aqWU4h#|U@(cG_=Qx+&xt~a zvCz~Ds3F71dsjNLkfM%TqdVNu=RNMOzh7?b+%hICbFlOAPphrYy>7D-e7{%o_kPFn z;T!?ilE-LcKM0P(GKMseEeW57Vs`=FF}(y@^pQl;rL3fHs8icmA+!6YJt&8 ztSF?%Un35qkv>drkks&BNTJv~xK?vD;aBkp7eIkDYqn+G0%;sT4FcwAoO+vke{8CO z0d76sgg$CannW5T#q`z~L4id)9BCKRU0A!Z-{HpXr)QJrd9@iJB+l32Ql)Z}*v(St zE)Vp=BB=DDB4Pr}B(UHNe31<@!6d{U?XDoxJ@S)9QM)2L%SA0x^~^fb=bdsBy!uh& zU?M_^kvnt%FZzm+>~bEH{2o?v&Iogs`1t-b+Ml`J!ZPS(46YQJKxWE81O$HE5w;** z|8zM%bp`M7J8)4;%DqH`wVTmM0V@D}xd%tRE3_6>ioMJxyi5Hkb>85muF81&EY!73ei zA3e<#ug||EZJ=1GLXNJ)A z791&ge#lF;GVX6IU?iw0jX^1bYaU?+x{zPlpyX6zijyn*nEdZ$fxxkl!a-~*P3bkf zPd*pzu~3GBYkR_>ET`5UM^>>zTV>5m>)f=az{d0sg6a8VzUtXy$ZS?h#Gk-CA?7)c zI%Vu9DN6XSDQn6;?n9`>l$q&>s?K)R8*OsmI+$L_m z_~E`}w694Z*`Xk3Ne=497Si~=RWRqCM?6=88smrxle#s*W znwhTRsMRmg?37GLJ-)%nDZA7r$YG849j8mJWir1bWBy& zZPneYojSbooC8U@tkO`bWx4%E5*;p#Q^1^S3lsfy7(6A{jL0`A__0vm?>xC%1y8_m z57FfWr^@YG2I1K7MGYuYd>JC}@sT2n^rkrY3w%~$J$Y~HSoOHn?zpR$ zjLj_bq@Yj8kd~DXHh30KVbz@K)0S;hPKm+S&-o%IG+@x@MEcrxW2KFh;z^4dJDZix zGRGe&lQD$p)0JVF4NRgGYuh0bYLy)BCy~sbS3^b3 zHixT<%-Vwbht|25T{3^Hk;qZ^3s!OOgljHs+EIf~C%=_>R5%vQI4mQR9qOXThMXlU zS|oSH>0PjnCakb*js2{ObN`}%HYsT6=%(xA| znpUtG_TJ08kHgm5l@G|t?4E3tG2fq?wNtIp*Vqrb{9@bo^~Rx7+J&OnayrX`LDcF~ zd@0m0ZJ#Z@=T>4kTa5e2FjI&5c(F7S{gnRPoGpu9eIqrtSvnT_tk$8T)r%YwZw!gK zj*k@cG)V&@t+mtDi37#>LhVGTfRA^p%x0d#_P|Mktz3*KOoLIqFm`~KGoDDD4OOxe z?}ag_c08u%vu=5Vx=~uoS8Q;}+R2~?Uh|m-+`-2kDo$d6T!nD*hc#dB(*R{LXV=zo z`PJP0V=O!@3l-bw+d`X6(=@fq=4O#ETa8M^fOvO4qja9o3e8ANc9$sI=A4$zUut~w z4+JryRkI{9qWxU1CCMM$@Aj=6)P+z?vqa=UCv_4XyVNoBD{Xb~Oi4cjjhm8fRD!*U z2)zaS;AI78^Wq+5mDInKiMz|z#K`2emQfNH*U;{9^{NqSMVoq?RSo43<8YpJM^+W$ zxy!A5>5Zl16Vi#?nAYywu3w_=KWnd3*QetocWt`3pK67>)ZVwnT3h zbPdD&MZkD?q=-N`MpCCwpM74L+Tr1aa)zJ)8G;(Pg51@U&5W>aNu9rA`bh{vgfE={ zdJ>aKc|2Ayw_bop+dK?Y5$q--WM*+$9&3Q9BBiwU8L<-`T6E?ZC`mT0b}%HR*LPK} z!MCd_Azd{36?Y_>yN{U1w5yrN8q`z(Vh^RnEF+;4b|2+~lfAvPT!`*{MPiDioiix8 zY*GdCwJ{S(5(HId*I%8XF=pHFz<9tAe;!D5$Z(iN#jzSql4sqX5!7Y?q4_%$lH zz8ehZuyl0K=E&gYhlfFWabnSiGty$>md|PpU1VfaC5~kskDnZX&Yu}?-h;OSav=8u z=e3Yq=mi$4A|sB-J00;1d{Sd1+!v0NtU((Nz2;PFFlC}V{@p&4wGcVhU&nI($RAS! zwXn7)?8~1J3*4+VccRSg5JS<(bBhBM&{ELMD4C_NTpvzboH!{Zr*%HP;{UqxI#g&7 zOAqPSW5Qus$8-xtTvD%h{Tw<2!XR(lU54LZG{)Cah*LZbpJkA=PMawg!O>X@&%+5XiyeIf91n2E*hl$k-Y(3iW*E}Mz-h~H~7S9I1I zR#-j`|Hk?$MqFhE4C@=n!hN*o5+M%NxRqP+aLxDdt=wS6rAu6ECK*;AB%Nyg0uyAv zO^DnbVZZo*|Ef{nsYN>cjZC$OHzR_*g%T#oF zCky9HJS;NCi=7(07tQXq?V8I&OA&kPlJ_dfSRdL2bRUt;tA3yKZRMHMXH&#W@$l%-{vQd7y@~i*^qnj^`Z{)V$6@l&!qP_y zg2oOd!Wit#)2A~w-eqw3*Mbe)U?N|q6sXw~E~&$!!@QYX4b@%;3=>)@Z#K^`8~Aki z+LYKJu~Y$;F5%_0aF9$MsbGS9Bz2~VUG@i@3Fi2q(hG^+Ia44LrfSfqtg$4{%qBDM z_9-O#3V+2~W$dW0G)R7l_R_vw(KSkC--u&%Rs^Io&*?R=`)6BN64>6>)`TxyT_(Rd zUn+aIl1mPa#Jse9B3`!T=|e!pIp$(8ZOe0ao?nS7o?oKlj zypC-fMj1DHIDrh1unUI1vp=-Fln;I9e7Jvs3wj*^_1&W|X} zZSL|S|Bb@CV*YC_-T&2!Ht3b6?)d`tHOP?rA;;t#zaXa0Sc;vGnV0BLIf8f-r{QHh z*Zp`4_ItlOR7{u(K+!p_oLDmaAkNag*l4#29F2b_A*0oz0T|#-&f*;c#<`^)(W@gm z#k9k=t%u8<+C1fNUA{Fh7~wgPrEZZ#(6aBI%6bR4RO(e1(ZocjoDek4#MTgZD>1NG zy9~yoZfWYfwe&S-(zk4o6q6o?2*~DOrJ(%5wSnEJMVOKCzHd z=Yhm+HLzoDl{P*Ybro7@sk1!Ez3`hE+&qr7Rw^2glw^M(b(NS2!F|Q!mi|l~lF94o z!QiV)Q{Z>GO5;l1y!$O)=)got;^)%@v#B!ZEVQy1(BJApHr5%Zh&W|gweD+%Ky%CO ztr45vR*y(@*Dg_Qw5v~PJtm^@Lyh*zRuT6~(K+^HWEF{;R#L$vL2!_ndBxCtUvZ(_ zauI7Qq}ERUWjr&XW9SwMbU>*@p)(cuWXCxRK&?ZoOy>2VESII53iPDP64S1pl{NsC zD;@EGPxs&}$W1;P6BB9THF%xfoLX|4?S;cu@$)9OdFst-!A7T{(LXtdNQSx!*GUSIS_lyI`da8>!y_tpJb3Zuf0O*;2y?HCfH z5QT6@nL|%l3&u4;F!~XG9E%1YwF*Fgs5V&uFsx52*iag(?6O|gYCBY3R{qhxT-Etb zq(E%V=MgQnuDGEKOGsmBj9T0-nmI%zys8NSO>gfJT4bP>tI>|ol@ zDt(&SUKrg%cz>AmqtJKEMUM;f47FEOFc%Bbmh~|*#E zDd!Tl(wa)ZZIFwe^*)4>{T+zuRykc3^-=P1aI%0Mh}*x7%SP6wD{_? zisraq`Las#y-6{`y@CU3Ta$tOl|@>4qXcB;1bb)oH9kD6 zKym@d$ zv&PZSSAV1Gwwzqrc?^_1+-ZGY+3_7~a(L+`-WdcJMo>EWZN3%z4y6JyF4NR^urk`c z?osO|J#V}k_6*9*n2?j+`F{B<%?9cdTQyVNm8D}H~T}?HOCXt%r7#2hz97Gx#X%62hyaLbU z_ZepP0<`<;eABrHrJAc!_m?kmu#7j}{empH@iUIEk^jk}^EFwO)vd7NZB=&uk6JG^ zC>xad8X$h|eCAOX&MaX<$tA1~r|hW?-0{t4PkVygTc`yh39c;&efwY(-#;$W)+4Xb z$XFsdG&;@^X`aynAMxsq)J#KZXX!sI@g~YiJdHI~r z$4mj_?S29sIa4c$z)19JmJ;Uj?>Kq=0XuH#k#};I&-6zZ_&>)j>UR0XetRO!-sjF< zd_6b1A2vfi++?>cf}s{@#BvTD|a%{9si7G}T+8ZnwuA z1k8c%lgE<-7f~H`cqgF;qZ|$>R-xNPA$25N1WI3#n%gj}4Ix}vj|e=x)B^roGQpB) zO+^#nO2 zjzJ9kHI6nI5ni&V_#5> z!?<7Qd9{|xwIf4b0bRc;zb}V4>snRg6*wl$Xz`hRDN8laL5tg&+@Dv>U^IjGQ}*=XBnXWrwTy;2nX?<1rkvOs#u(#qJ=A zBy>W`N!?%@Ay=upXFI}%LS9bjw?$h)7Dry0%d}=v0YcCSXf9nnp0tBKT1eqZ-4LU` zyiXglKRX)gtT0VbX1}w0f2ce8{$WH?BQm@$`ua%YP8G@<$n13D#*(Yd5-bHfI8!on zf5q4CPdgJLl;BqIo#>CIkX)G;rh|bzGuz1N%rr+5seP${mEg$;uQ3jC$;TsR&{IX< z;}7j3LnV+xNn^$F1;QarDf6rNYj7He+VsjJk6R@0MAkcwrsq4?(~`GKy|mgkfkd1msc2>%B!HpZ~HOzj}kl|ZF(IqB=D6ZTVcKe=I7)LlAI=!XU?J*i#9VXeKeaG zwx_l@Z(w`)5Cclw`6kQKlS<;_Knj)^Dh2pL`hQo!=GPOMR0iqEtx12ORLpN(KBOm5 zontAH5X5!9WHS_=tJfbACz@Dnkuw|^7t=l&x8yb2a~q|aqE_W&0M|tI7@ilGXqE)MONI8p67OiQGqKEQWw;LGga=ZM1;{pSw1jJK_y$vhY6 ztFrV7-xf>lbeKH1U)j3R=?w*>(Yh~NNEPVmeQ8n}0x01$-o z2Jyjn+sXhgOz>AzcZ zAbJZ@f}MBS0lLKR=IE{z;Fav%tcb+`Yi*!`HTDPqSCsFr>;yt^^&SI2mhKJ8f*%ji zz%JkZGvOn{JFn;)5jf^21AvO-9nRzsg0&CPz;OEn07`CfT@gK4abFBT$Mu?8fCcscmRkK+ zbAVJZ~#_a z{|(FFX}~8d3;DW8zuY9?r#Dt>!aD>} zlYw>D7y#eDy+PLZ&XKIY&Df0hsLDDi(Yrq8O==d30RchrUw8a=Eex>Dd?)3+k=}Q> z-b85lun-V$I}86Vg#l1S@1%=$2BQD5_waAZKQfJ${3{b2SZ#w1u+jMr{dJMvI|Og= zpQ9D={XK|ggbe04zTUd}iF{`GO1dV%zWK~?sM9OM(= zVK9&y4F^w1WFW{$qi|xQk0F`@HG8oLI5|5$j~ci9xTMT69v5KS-Yym--raU5kn2#C z<~5q^Bf0rTXVhctG2%&MG(cUGaz(gC(rcG~>qgO$W6>!#NOVQJ;pIYe-lLy(S=HgI zPh;lkL$l+FfMHItHnw_^bj8}CKM19t(C_2vSrhX2$K@-gFlH};#C?1;kk&U1L%4S~ zR^h%h+O1WE7DI$~dly?-_C7>(!E`~#REJ~Xa7lyrB$T!`&qYV5QreAa^aKr%toUJR zPWh)J3iD`(P6BI5k$oE$us#%!4$>`iH2p-88?WV0M$-K)JDibvA4 zpef%_*txN$Ei3=Lt(BBxZ&mhl|mUz-z*OD1=r9nfN zc5vOMFWpi>K=!$6f{eb?5Ru4M3o;t9xLpry|C%j~`@$f)OFB5+xo8XM8g&US@UU-sB|dAoc20y(F@=-2Ggp_`SWjEb#>IG^@j zuQK}e^>So#W2%|-)~K!+)wdU#6l>w5wnZt2pRL5Dz#~N`*UyC9tYechBTc2`@(OI# zNvcE*+zZZjU-H`QOITK^tZwOyLo)ZCLk>>Wm+flMsr5X{A<|m`Y281n?8H_2Fkz5}X?i%Rfm5s+n`J zDB&->=U+LtOIJ|jdYXjQWSQZFEs>Rm{`knop4Sq)(}O_@gk{14y51)iOcGQ5J=b#e z2Yx^6^*F^F7q_m-AGFFgx5uqyw6_4w?yKCJKDGGprWyekr;X(!4CnM5_5?KgN=3qCm03 z##6k%kIU5%g!cCL(+aK>`Wd;dZ4h$h_jb7n?nqx5&o9cUJfr%h#m4+Bh)>HodKcDcsXDXwzJ3jR(sSFqWV(OKHC*cV8;;&bH=ZI0YbW3PgIHwTjiWy z?2MXWO2u0RAEEq(zv9e%Rsz|0(OKB?_3*kkXwHxEuazIZ7=JhaNV*P~hv57q55LoebmJpfHXA@yuS{Esg+ z*C}0V-`x^=0nOa@SPUJek>td~tJ{U1T&m)~`FLp*4DF77S^{|0g%|JIqd-=5)p6a` zpJOsEkKT(FPS@t^80V!I-YJbLE@{5KmVXjEq{QbCnir%}3 zB)-J379=wrBNK6rbUL7Mh^tVmQYn-BJJP=n?P&m-7)P#OZjQoK0{5?}XqJScV6>QX zPR>G{xvU_P;q!;S9Y7*07=Z!=wxIUorMQP(m?te~6&Z0PXQ@I=EYhD*XomZ^z;`Os z4>Uh4)Cg2_##mUa>i1Dxi+R~g#!!i{?SMj%9rfaBPlWj_Yk)lCV--e^&3INB>I?lu z9YXCY5(9U`3o?w2Xa5ErMbl5+pDVpu8v+KJzI9{KFk1H?(1`_W>Cu903Hg81vEX32l{nP2vROa1Fi!Wou0+ZX7Rp`g;B$*Ni3MC-vZ`f zFTi7}c+D)!4hz6NH2e%%t_;tkA0nfkmhLtRW%){TpIqD_ev>}#mVc)<$-1GKO_oK8 zy$CF^aV#x7>F4-J;P@tqWKG0|D1+7h+{ZHU5OVjh>#aa8+V;6BQ)8L5k9t`>)>7zr zfIlv77^`Fvm<)_+^z@ac%D&hnlUAFt8!x=jdaUo{)M9Ar;Tz5Dcd_|~Hl6CaRnK3R zYn${wZe8_BZ0l0c%qbP}>($jsNDay>8+JG@F!uV4F;#zGsBP0f$f3HqEHDz_sCr^q z1;1}7KJ9&`AX2Qdav1(nNzz+GPdEk5K3;hGXe{Hq13{)c zZy%fFEEH#nlJoG{f*M^#8yXuW%!9svN8ry-Vi7AOFnN~r&D`%6d#lvMXBgZkX^vFj z;tkent^62jUr$Cc^@y31Lka6hS>F?1tE8JW$iXO*n9CQMk}D*At3U(-W1E~z>tG?> z5f`5R5LbrhRNR8kv&5d9SL7ke2a*Xr)Qp#75 z6?-p035n2<7hK;sb>t9GAwG4{9v~iEIG>}7B5zcCgZhu$M0-z8?eUO^E?g)md^XT_ z2^~-u$yak>LBy(=*GsTj6p<>b5PO&un@5hGCxpBQlOB3DpsItKZRC*oXq-r{u}Wb; z&ko>#fbnl2Z;o@KqS-d6DTeCG?m1 z&E>p}SEc*)SD&QjZbs!Csjx~0+$@ekuzV_wAalnQvX3a^n~3ui)|rDO+9HW|JPEeBGP4 z)?zcZ<8qv47`EWA*_X~H^vr(lP|f%=%cWFM;u)OFHruKT<~?>5Y8l?56>&;=WdZU# zZEK4-C8s-3zPMA^&y~e*9z)!ZJghr3N^pJa2A$??Xqx-BR*TytGYor&l8Q+^^r%Yq02xay^f#;;wO6K7G!v>wRd6531WnDI~h$PN( z+4#08uX?r&zVKsQ;?5eBX=FxsXaGyH4Gth4a&L|{8LnNCHFr1M{KjJ!BfBS_aiy-E zxtmNcXq3}WTwQ7Dq-9YS5o758sT(5b`Sg-NcH>M9OH1oW6&sZ@|GYk|cJI`vm zO<$~q!3_$&GfWetudRc*mp8)M)q7DEY-#@8w=ItkApfq3sa)*GRqofuL7)dafznKf zLuembr#8gm*lIqKH)KMxSDqbik*B(1bFt%3Vv|ypehXLCa&wc7#u!cJNlUfWs8iQ` z$66(F=1fkxwg745-8_eqV>nWGY3DjB9gE23$R5g&w|C{|xvT@7j*@aZNB199scGchI7pINb5iyqYn)O=yJJX)Ca3&Ca+{n<=1w|(|f0)h<9gs$pVSV<<9Og-V z8ki@nKwE)x)^wmHBMk?mpMT=g{S#^8W|>&rI#Ceh;9za}io0k@0JxiCqi-jHlxbt3 zjJA?RihhRvhk6%G5-D{ePh1jare*fQS<328P-DcVAxPTrw=n6k?C6EV75f}cnBRPT zMYDqqKu(ND&aOtc!QRV`vzJSVxx8i~WB#5Ml{b#eQqNnSi7l-bS-`ITW<^zyYQA(b zbj4SuRK>q9o`_v%+C=S?h>2e4!66Ij(P5{7Uz$3u6YJJC$W%EoBa{-(=tQ|y1vov%ZkXVOV z##_UVg4V^4ne#4~<-1DkJqkKqgT+E_=&4Ue&eQ-JC+gi?7G@d6= zximz{zE)WW{b@QCJ!7l&N5x=dXS?$5RBU-VvN4Uec-GHK&jPa&P2z+qDdLhIB+HU) zu0CW&uLvE^4I5xtK-$+oe|58)7m6*PO%Xt<+-XEA%jG_BEachkF3e@pn?tl!`8lOF zbi2QOuNXX)YT*MCYflILO{VZ*9GiC%R4FO20zMK?p+&aCMm2oeMK7(aW=UDzr=AO0 z$5mJ%=qRsR8rZ>_YsL+vi{3*J_9Kzq(;ZwRj+4_f0-*wbkSMPWahX#Fj_a8BnrhJ6 zo^ZZ?Vah1@&6#r=JkuaYDBdp;J3@ii+CHM&@9*er&#P}$@wI$bfrH)&c!*|nkvhf%^*Y6b%dKz%QBSIo@U z{?V^qEs4`q<8@n+u8YiB^sc@6g>TncG<|GsmC3egwE6aO=EwLr~3-2 zNr`+)`i+-83?|1Xy0^8ps&pb}YT?w1eWVnC9Ps1=KM;Rw)bH6O!7Did1NwpnqVPZc z*%Qo~qkDL>@^<^fmIBtx$WUWQiNtAB2x-LO^BB=|w~-zTnJNEdm1Ou(?8PF&U88X@ z#8rdaTd||)dG^uJw~N_-%!XNbuAyh4`>Shea=pSj0TqP+w4!`nxsmVSv02kb`DBr% zyX=e>5IJ3JYPtdbCHvKMdhXUO_*E9jc_?se7%VJF#&ZaBD;7+eFN3x+hER7!u&`Wz z7zMvBPR4y`*$a250KYjFhAKS%*XG&c;R-kS0wNY1=836wL6q02mqx;IPcH(6ThA@2 zXKQF|9H>6AW$KUF#^A%l6y5{fel77_+cR_zZ0(7=6bmNXABv}R!B-{(E^O6Y?ZS)n zs1QEmh_Fm7p}oRyT3zxUNr4UV8NGs+2b8|4shO$OGFj3D&7_e?#yDi=TTe%$2QbG5 zk<;q7aQ;p!M-Osm{vFdmXZ@!z9uWh!;*%>(vTRggufuUGP9Hols@vhx z73pn$3u2;vzRvnXuT&$Os7J@6y12*j!{ix%3B4YU1466ItmJs0NsU(4ZYRYh7wEA6q{b*Hs6@k~ zi7Yq@Ax!et0cUMTvk7P%ym){MHpcliHEI~e3HP0NV=}7;xFv#IC?a<=`>~j_sk{e> z7vg-tK*p83HZ0=QK@ zRIHo^r{D8&Ms-^WZp+6US_Quqjh$Q66W^1}=Uz&XJ8AQE9&2}P zY|FXZzZ|0IiaBd2qdt6dIjQr(ZMIOU%NG1F&fu6Po9m^?BvLhI6T0R!H2d8;U(&p2 zYA|MFscMqcO(ye~Jp?F;0>Ke+5hzVr?aBNe>GsGgr$XrpS9uajN2kNQ3o$V5rp0T( z0$6TJC;3)26SNG#XcX7l^MKTn$ga?6r4Jzfb%ZgA(Zbwit0$kY=avSnI$@Gk%+^pu zS5mHrcRS8LFPC*uVWH4DDD1pY$H8N>X?KIJZuZ2SvTqc5Nr0GHdD8TCJcd$zIhOdC zZX0ErnsozQh;t^==4zTfrZO421AL?)O)l#GSxU#|LTTg4#&yeK=^w#;q63!Nv~1(@ zs^-RNRuF&qgcr+bIzc@7$h9L;_yjdifE*$j0Q&Np=1AuHL--zdkv@}`1 zo~LlDl_YAq*z?vmr4M`GjDkl9?p|-tl(DtX76oZv25_DtZutLS9Ez!5~p?th@4 zyc_uax4W#<(#)LMkvo)yp|5tKsC2=p#6PyhpH|449T<9Zdk|%CAb5cw?fhvQtBO&7 zpQ9$24yLqPHP;$N&fe2wm%8qdctwIna<3SwGtQA3{C77s%CW%LYxtK(SBGustL0<( zu~U9r0UOkr(c{OJxZS0Ntu3+cJlF7R`7k-Bsa&q?9Ae5{{|o~?cM+T7{lB1^#vT8R z?>c9fNWey`1dKDY%F3d2O*8^qYhjlB8*7HMKE<*=(A`{>=1%s1}Pm&#_t1xy!FkPk@%SMEka2@*= zxDuM|vJJ5s+xgDls{>*o!7eOcs|xuVBPWX&+y5vEiADK%hi`#Dbd>;;Pbk2H4*-X&R?_-6ZEutSd8hC+sSjhIo z;D(j4P;2EVpEj#UF7IjM6PC+X$C5T&=nL`*!*hm9U)#O?>wqOgC>jXKN3Slk_yaQX zLf|4D8T4k|wHW`;#ZQVocNF|3izi0sOqXzi7@KlYC3CXBG`94wD;tMI1bj|8Vm zY}9`VI9!plSfhAal$M_HlaYOVNU?9Z#0<$o?lXXbX3O(l_?f)i3_~r+GcO-x#+x^X zfsZl0>Rj2iP1rsT;+b;Mr? z4Vu&O)Q5ru4j;qaSP5gA{az@XTS1NpT0d9Xhl_FkkRpcEGA0(QQ~YMh#&zwDUkNzm z6cgkdgl9W{iL6ArJ1TQHqnQ^SQ1WGu?FT|93$Ba}mPCH~!$3}0Y0g zcoG%bdTd$bmBx9Y<`Jc+=Cp4}c@EUfjiz;Rcz101p z=?#i$wo>gBE9|szaZMt-d4nUIhBnYRuBVyx+p?5#aZQgUe(!ah`J#l1$%bl5avL27 zU2~@V`3Ic&!?FhDX@Cw!R4%xtWark#p8DLT)HCZ?VJxf^yr@AD*!ERK3#L$E^*Yr? zzN&uF9Roh4rP+r`Z#7U$tzl6>k!b~HgM$C<_crP=vC>6=q{j?(I}!9>g3rJU(&){o z`R^E*9%+kEa8H_fkD9VT7(Fks&Y-RcHaUJYf-|B+eMXMaRM;{FKRiTB>1(=Iij4k1(X__|WqAd-~t#2@UQ}Z&<1Th0azdXfoll!dd)6>1miA z!&=6sDJm=e$?L&06+Q3`D-HNSkK-3$3DdZMX-6Xjn;wd#9A{~ur!2NcX>(qY_oZL0~H7dnQ9sgLe!W>~2|RSW7|hWn<({Pg*xF$%B-!rKe^_R_vc z(LO!0agxxP;FWPV({8#lEv$&&GVakGus=@!3YVG`y^AO1m{2%Np;>HNA1e{=?ra1C}H zAwT0sbwG|!am;fl?*_t^^#yLDXZ*Nx)_FqueZi0c-G~omtpHW0Cu)mEJ`Z1X8brq$ z%vK##b~o*^b&Hz!hgrD=^6P8}aW40lhzMLB5T5*v`1QH?+L~-@CDi3+C@nRf2{7UE zyDIe{@LKw`Eu=Z%6<<_=#V|yxJIKiq_N?ZJ_v0$c)N4l07ZV_mIXG}glfBSPivOhw z-~+9GdckSpMBNR9eR`Y|9_)sXS+u_OiQ%!9rE(2AFjoxN8lk16Sb~^Sq6kRoEp3yD(mm`HsYIXcag_EAB8MHc}nahxVVUTts~U9P|f;7Ul$_` zStR4v&P4q_$KXOEni$lkxy8=9w8G&47VY0oDb^+jT+>ARe3NHUg~St`$RDxY)?;_F znqTujR&chZd2qHF7y8D$4&E3+e@J~!X3&BW4BF(Ebp#TEjrd+9SU!)j;qH+ZkL@AW z?J6Mj}v0_+D zH0qlbzCkHf|EZ`6c>5ig5NAFF%|La%M-}g(7&}Vx8K)qg30YD;H!S!??{;YivzrH0 z(M%2*b_S-)yh&Aiqai)GF^c!<1Xemj|13>dZ_M#)41SrP;OEMaRJ)bCeX*ZT7W`4Y zQ|8L@NHpD@Tf(5>1U(s5iW~Zdf7$@pAL`a3X@YUv1J>q-uJ_(Dy5nYTCUHC}1(dlI zt;5>DLcHh&jbysqt?G01MhXI3!8wgf){Hv}=0N|L$t8M#L7d6WscO8Om2|NBz2Ga^ zs86y%x$H18)~akOWD7@em7)ldlWgb?_sRN>-EcYQO_}aX@+b$dR{146>{kXWP4$nN{V0_+|3{Lt|8uX_fhKh~i{(x%cj*PU$i{PO(5$uA? zQzO>a6oPj-TUk&{zq?JD2MNb6Mf~V3g$ra+PB;ujLJ2JM(a7N*b`y{MX--!fAd}5C zF$D_b8S;+Np(!cW)(hnv5b@@|EMt*RLKF*wy>ykFhEhlPN~n_Bj>LT9B^_yj>z#fx z3JuE4H&?Cc!;G@}E*3k`HK#8ag`yE3Z1)5JUlSua%qkF zkTu|<9{w9OSi$qr)WD#7EzITnch=xnR63E*d~WGvi*Co9BBE?ETHud;!Z)7&wz+l6 zuKODYG1>I1U#a%&(GNJ`AqRfg=H!BtSl+_;CEeufF-#+*2EMMz-22@>18=8PH{PHd z);mN=aR0MPF>eutLiS#-AOX>#2%+pTGEOj!j4L(m0~&xR=0+g#HNpno6@veLhJp}e zyNVC$a>4;!9&iGvU_dj&xbKt@^t6r%f^)+}eV^suRTLP52+BVs0kOLwg6n`=NUv50E7My8XQUh?y%mW62OT1pMrKI3Q(r`7vU&@93=G~A?b(^pvC-8x=bSk zZ60BQR96WB1Z@9Df(M1IQh+YrU8sEjB=Tc2;(zBn-pete*icZE|M&Uc+oHg`|1o`g zH~m+k=D$o);{Rs)b<9Zo|9_Z6L6QHLNki(N>Dw^^i1LITprZeeqIaT#+)fw)PlllU zldphHC)t!0Gf(i9zgVm>`*TbmITF zH1FZ4{wrjRCx{t^26VK_2srZuWuY*EMAsMrJYFFCH35Ky7bq8<0K|ey2wHnrFMZyr z&^yEgX{{3i@&iE5>xKZ{Ads36G3a!i50D!C4?^~cLB<<|fc1!XN(HJRM)H^21sEs%vv+Mu0h*HkLHaEffMwc0n6)JhNXY#M5w@iO@dfXY z0c6dM2a4Hd1SA*#qYj@jK}uVgAZdaBj8t6uuhUNe>)ne9vfd#C6qLV9+@Q7{MnF#0 zJ7fd-ivG_~u3bVvOzpcw1u~ZSp8-kl(sunnX>L~*K-ByWDM2E8>;Si6kn^58AZQxI xVa^It*?521mj4+UJO?7%w*+`EfEcU=@KhDx-s^WzP+ae~{CgHDE&XryzW}Nww%-5% diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index aa991fce..ae04661e 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 1b6c7873..a69d9cb6 100755 --- a/gradlew +++ b/gradlew @@ -205,6 +205,12 @@ set -- \ org.gradle.wrapper.GradleWrapperMain \ "$@" +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. diff --git a/gradlew.bat b/gradlew.bat index 107acd32..f127cfd4 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,7 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -75,13 +75,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/src/androidMain/kotlin/com/configcat/Cache.kt b/src/androidMain/kotlin/com/configcat/Cache.kt index 2f5c81ed..008d2a6f 100644 --- a/src/androidMain/kotlin/com/configcat/Cache.kt +++ b/src/androidMain/kotlin/com/configcat/Cache.kt @@ -14,7 +14,8 @@ public class SharedPreferencesCache(context: Context) : ConfigCache { private val sharedPreferences: SharedPreferences init { - sharedPreferences = context.applicationContext.getSharedPreferences("configcat_preferences", Context.MODE_PRIVATE) + sharedPreferences = + context.applicationContext.getSharedPreferences("configcat_preferences", Context.MODE_PRIVATE) } override suspend fun read(key: String): String? = sharedPreferences.getString(key, null) diff --git a/src/androidMain/kotlin/com/configcat/NumberFormatter.kt b/src/androidMain/kotlin/com/configcat/NumberFormatter.kt new file mode 100644 index 00000000..b342f890 --- /dev/null +++ b/src/androidMain/kotlin/com/configcat/NumberFormatter.kt @@ -0,0 +1,35 @@ +package com.configcat + +import java.text.DecimalFormat +import java.text.DecimalFormatSymbols +import java.util.* +import kotlin.math.abs + +internal actual fun doubleToString(doubleToString: Double): String { + // Handle Double.NaN, Double.POSITIVE_INFINITY and Double.NEGATIVE_INFINITY + if (doubleToString.isNaN() || doubleToString.isInfinite()) { + return doubleToString.toString() + } + + // To get similar result between different SDKs the Double value format is modified. + // Between 1e-6 and 1e21 we don't use scientific-notation. Over these limits scientific-notation used but the + // ExponentSeparator replaced with "e" and "e+". + // "." used as decimal separator in all cases. + val abs = abs(doubleToString) + val fmt = + if (1e-6 <= abs && abs < 1e21) DecimalFormat("#.#################") else DecimalFormat("#.#################E0") + val symbols = DecimalFormatSymbols.getInstance(Locale.UK) + if (abs > 1) { + symbols.exponentSeparator = "e+" + } else { + symbols.exponentSeparator = "e" + } + fmt.decimalFormatSymbols = symbols + return fmt.format(doubleToString) +} + +internal actual fun formatDoubleForLog(doubleToFormat: Double): String { + val decimalFormat = DecimalFormat("0.#####") + decimalFormat.decimalFormatSymbols = DecimalFormatSymbols.getInstance(Locale.UK) + return decimalFormat.format(doubleToFormat) +} diff --git a/src/androidMain/kotlin/com/configcat/fetch/HttpRequestBuilder.kt b/src/androidMain/kotlin/com/configcat/fetch/HttpRequestBuilder.kt new file mode 100644 index 00000000..162c1103 --- /dev/null +++ b/src/androidMain/kotlin/com/configcat/fetch/HttpRequestBuilder.kt @@ -0,0 +1,10 @@ +package com.configcat.fetch + +import io.ktor.client.request.* + +internal actual fun httpRequestBuilder( + configCatUserAgent: String, + eTag: String +): HttpRequestBuilder { + return commonHttpRequestBuilder(configCatUserAgent, eTag) +} diff --git a/src/androidMain/kotlin/com/configcat/override/SettingConverter.kt b/src/androidMain/kotlin/com/configcat/override/SettingConverter.kt new file mode 100644 index 00000000..607baf5c --- /dev/null +++ b/src/androidMain/kotlin/com/configcat/override/SettingConverter.kt @@ -0,0 +1,7 @@ +package com.configcat.override + +import com.configcat.model.Setting + +internal actual fun convertToSetting(value: Any): Setting { + return commonConvertToSetting(value) +} diff --git a/src/commonMain/kotlin/com/configcat/Config.kt b/src/commonMain/kotlin/com/configcat/Config.kt deleted file mode 100644 index 8c585b85..00000000 --- a/src/commonMain/kotlin/com/configcat/Config.kt +++ /dev/null @@ -1,194 +0,0 @@ -package com.configcat - -import com.soywiz.klock.DateTime -import kotlinx.serialization.* -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder -import kotlinx.serialization.json.* - -internal data class Entry( - val config: Config, - val eTag: String, - val configJson: String, - val fetchTime: DateTime -) { - fun isEmpty(): Boolean = this === empty - - companion object { - val empty: Entry = Entry(Config.empty, "", "", Constants.distantPast) - - fun fromString(cacheValue: String?): Entry { - if (cacheValue.isNullOrEmpty()) { - return empty - } - val fetchTimeIndex = cacheValue.indexOf("\n") - val eTagIndex = cacheValue.indexOf("\n", fetchTimeIndex + 1) - require(fetchTimeIndex > 0 && eTagIndex > 0) { "Number of values is fewer than expected." } - val fetchTimeRaw = cacheValue.substring(0, fetchTimeIndex) - require(DateTimeUtils.isValidDate(fetchTimeRaw)) { "Invalid fetch time: $fetchTimeRaw" } - val fetchTimeUnixMillis = fetchTimeRaw.toLong() - val eTag = cacheValue.substring(fetchTimeIndex + 1, eTagIndex) - require(eTag.isNotEmpty()) { "Empty eTag value." } - val configJson = cacheValue.substring(eTagIndex + 1) - require(configJson.isNotEmpty()) { "Empty config jsom value." } - return try { - val config: Config = Constants.json.decodeFromString(configJson) - Entry(config, eTag, configJson, DateTime(fetchTimeUnixMillis)) - } catch (e: Exception) { - throw IllegalArgumentException("Invalid config JSON content: $configJson", e) - } - } - } - - fun serialize(): String { - return "${fetchTime.unixMillis.toLong()}\n${eTag}\n$configJson" - } -} - -@Serializable -internal data class Config( - @SerialName("p") - val preferences: Preferences? = null, - @SerialName("f") - val settings: Map -) { - internal fun isEmpty(): Boolean = this == empty - - internal companion object { - val empty: Config = Config(null, mapOf()) - } -} - -@Serializable -internal data class Preferences( - @SerialName("u") - val baseUrl: String, - @SerialName("r") - val redirect: Int -) - -/** Describes a feature flag / setting. */ -@Serializable -public data class Setting( - /** Value of the feature flag / setting. */ - @Contextual - @SerialName("v") - val value: Any, - - /** - * Type of the feature flag / setting. - * - * 0 -> [Boolean], - * 1 -> [String], - * 2 -> [Int], - * 3 -> [Double], - */ - @SerialName("t") - val type: Int = 0, - - /** Collection of percentage rules that belongs to the feature flag / setting. */ - @SerialName("p") - val percentageItems: List = listOf(), - - /** Collection of targeting rules that belongs to the feature flag / setting. */ - @SerialName("r") - val rolloutRules: List = listOf(), - - /** Variation ID (for analytical purposes). */ - @SerialName("i") - val variationId: String? = null -) - -/** Describes a percentage rule. */ -@Serializable -public data class PercentageRule( - /** Value served when the rule is selected during evaluation. */ - @Contextual - @SerialName("v") - val value: Any, - - /** The rule's percentage value. */ - @SerialName("p") - val percentage: Double, - - /** The rule's variation ID (for analytical purposes). */ - @SerialName("i") - val variationId: String? = null -) - -/** Describes a targeting rule. */ -@Serializable -public data class RolloutRule( - /** Value served when the rule is selected during evaluation. */ - @Contextual - @SerialName("v") - val value: Any, - - /** The user attribute used in the comparison during evaluation. */ - @SerialName("a") - val comparisonAttribute: String, - - /** - * The operator used in the comparison. - * - * 0 -> 'IS ONE OF', - * 1 -> 'IS NOT ONE OF', - * 2 -> 'CONTAINS', - * 3 -> 'DOES NOT CONTAIN', - * 4 -> 'IS ONE OF (SemVer)', - * 5 -> 'IS NOT ONE OF (SemVer)', - * 6 -> '< (SemVer)', - * 7 -> '<= (SemVer)', - * 8 -> '> (SemVer)', - * 9 -> '>= (SemVer)', - * 10 -> '= (Number)', - * 11 -> '<> (Number)', - * 12 -> '< (Number)', - * 13 -> '<= (Number)', - * 14 -> '> (Number)', - * 15 -> '>= (Number)', - * 16 -> 'IS ONE OF (Sensitive)', - * 17 -> 'IS NOT ONE OF (Sensitive)' - */ - @SerialName("t") - val comparator: Int, - - /** The comparison value compared to the given user attribute. */ - @SerialName("c") - val comparisonValue: String, - - /** The rule's variation ID (for analytical purposes). */ - @SerialName("i") - val variationId: String? = null -) - -internal object FlagValueSerializer : KSerializer { - override fun deserialize(decoder: Decoder): Any { - val json = decoder as? JsonDecoder - ?: error("Only JsonDecoder is supported.") - val element = json.decodeJsonElement() - val primitive = element as? JsonPrimitive ?: error("Unable to decode $element") - return when (primitive.content) { - "true", "false" -> primitive.content == "true" - else -> primitive.content.toIntOrNull() ?: primitive.content.toDoubleOrNull() ?: primitive.content - } - } - - override fun serialize(encoder: Encoder, value: Any) { - val json = encoder as? JsonEncoder - ?: error("Only JsonEncoder is supported.") - val element: JsonElement = when (value) { - is String -> JsonPrimitive(value) - is Number -> JsonPrimitive(value) - is Boolean -> JsonPrimitive(value) - is JsonElement -> value - else -> throw IllegalArgumentException("Unable to encode $value") - } - json.encodeJsonElement(element) - } - - @OptIn(ExperimentalSerializationApi::class) - override val descriptor: SerialDescriptor = - ContextualSerializer(Any::class, null, emptyArray()).descriptor -} diff --git a/src/commonMain/kotlin/com/configcat/ConfigCache.kt b/src/commonMain/kotlin/com/configcat/ConfigCache.kt index f7837e22..390a0172 100644 --- a/src/commonMain/kotlin/com/configcat/ConfigCache.kt +++ b/src/commonMain/kotlin/com/configcat/ConfigCache.kt @@ -17,7 +17,8 @@ public interface ConfigCache { internal class EmptyConfigCache : ConfigCache { override suspend fun read(key: String): String? = null - override suspend fun write(key: String, value: String) { /* do nothing */ } + override suspend fun write(key: String, value: String) { /* do nothing */ + } } internal expect fun defaultCache(): ConfigCache diff --git a/src/commonMain/kotlin/com/configcat/ConfigCatClient.kt b/src/commonMain/kotlin/com/configcat/ConfigCatClient.kt index 6094b732..39703429 100644 --- a/src/commonMain/kotlin/com/configcat/ConfigCatClient.kt +++ b/src/commonMain/kotlin/com/configcat/ConfigCatClient.kt @@ -1,8 +1,11 @@ package com.configcat +import com.configcat.Client.SettingTypeHelper.toSettingTypeOrNull import com.configcat.fetch.ConfigFetcher import com.configcat.fetch.RefreshResult import com.configcat.log.* +import com.configcat.model.Setting +import com.configcat.model.SettingType import com.configcat.override.FlagOverrides import com.configcat.override.OverrideBehavior import com.soywiz.klock.DateTime @@ -86,6 +89,9 @@ public class ConfigCatOptions { public var hooks: Hooks = Hooks() internal var sdkKey: String? = null + internal fun isBaseURLCustom(): Boolean { + return !baseUrl.isNullOrEmpty() + } } /** @@ -94,24 +100,33 @@ public class ConfigCatOptions { public interface ConfigCatClient { /** * Gets the value of a feature flag or setting as [Any] identified by the given [key]. - * In case of any failure, [defaultValue] will be returned. The [user] param identifies the caller. + * + * @param key the identifier of the feature flag or setting. + * @param defaultValue in case of any failure, this value will be returned. + * @param user the user object. */ - public suspend fun getAnyValue(key: String, defaultValue: Any, user: ConfigCatUser?): Any + public suspend fun getAnyValue(key: String, defaultValue: Any?, user: ConfigCatUser?): Any? /** * Gets the value and evaluation details of a feature flag or setting identified by the given [key]. - * The [user] param identifies the caller. + * + * @param key the identifier of the feature flag or setting. + * @param defaultValue in case of any failure, this value will be returned. + * @param user the user object. */ - public suspend fun getAnyValueDetails(key: String, defaultValue: Any, user: ConfigCatUser?): EvaluationDetails + public suspend fun getAnyValueDetails(key: String, defaultValue: Any?, user: ConfigCatUser?): EvaluationDetails /** * Gets the values along with evaluation details of all feature flags and settings. - * The [user] param identifies the caller. + * + * @param user the user object. */ public suspend fun getAllValueDetails(user: ConfigCatUser? = null): Collection /** * Gets the key of a setting and its value identified by the given [variationId] (analytics). + * + * @param variationId the Variation ID. */ public suspend fun getKeyAndValue(variationId: String): Pair? @@ -122,21 +137,23 @@ public interface ConfigCatClient { /** * Gets the values of all feature flags or settings. The [user] param identifies the caller. + * + * @param user the user object. */ - public suspend fun getAllValues(user: ConfigCatUser? = null): Map + public suspend fun getAllValues(user: ConfigCatUser? = null): Map /** - * Downloads the latest feature flag and configuration values. + * Initiates a force refresh on the cached configuration. */ public suspend fun forceRefresh(): RefreshResult /** - * Configures the SDK to allow HTTP requests. + * Configures the SDK to not initiate HTTP requests and work only from its cache. */ public fun setOnline() /** - * Configures the SDK to not initiate HTTP requests. + * Set the client to offline mode. HTTP calls are not allowed. */ public fun setOffline() @@ -152,6 +169,10 @@ public interface ConfigCatClient { /** * Sets the default user. + * If no user specified in the following calls [getValue], [getAnyValue], [getAllValues], [getValueDetails], + * [getAnyValueDetails], [getAllValueDetails] the default user value will be used. + * + * @param user The new default user. */ public fun setDefaultUser(user: ConfigCatUser) @@ -191,38 +212,91 @@ public fun ConfigCatClient( /** * Gets the value of a feature flag or setting as [T] identified by the given [key]. - * In case of any failure, [defaultValue] will be returned. The [user] param identifies the caller. + * + * @param key the identifier of the feature flag or setting. + * @param defaultValue in case of any failure, this value will be returned. + * @param user the user object. + * @param T the type of the desired feature flag or setting. Only the following types are allowed: [String], + * [Boolean], [Int] and [Double] (both nullable and non-nullable). */ -public suspend inline fun ConfigCatClient.getValue( +public suspend inline fun ConfigCatClient.getValue( key: String, defaultValue: T, user: ConfigCatUser? = null -): T = this.getAnyValue(key, defaultValue, user) as? T ?: defaultValue +): T { + require( + T::class == Boolean::class || + T::class == String::class || + T::class == Int::class || + T::class == Double::class + ) { + "Only the following types are supported: String, Boolean, Int, Double (both nullable and non-nullable)." + } + + return getValueInternal(this, key, defaultValue, user) as T +} + +@PublishedApi +internal suspend fun getValueInternal( + configCatClient: ConfigCatClient, + key: String, + defaultValue: Any?, + user: ConfigCatUser? +): Any? { + val client = configCatClient as? Client + return client?.getValueImpl(key, defaultValue, user, allowAnyReturnType = false) + ?: configCatClient.getAnyValue(key, defaultValue, user) +} /** * Gets the value and evaluation details of a feature flag or setting identified by the given [key]. - * The [user] param identifies the caller. + * + * @param key the identifier of the feature flag or setting. + * @param defaultValue in case of any failure, this value will be returned. + * @param user the user object. + * @param T the type of the desired feature flag or setting. Only the following types are allowed: [String], + * [Boolean], [Int] and [Double] (both nullable and non-nullable). */ -public suspend inline fun ConfigCatClient.getValueDetails( +public suspend inline fun ConfigCatClient.getValueDetails( key: String, defaultValue: T, user: ConfigCatUser? = null ): TypedEvaluationDetails { - val details = this.getAnyValueDetails(key, defaultValue, user) - val value = details.value as? T + require( + T::class == Boolean::class || + T::class == String::class || + T::class == Int::class || + T::class == Double::class + ) { + "Only the following types are supported: String, Boolean, Int, Double (both nullable and non-nullable)." + } + + val details = getValueDetailsInternal(this, key, defaultValue, user) return TypedEvaluationDetails( details.key, details.variationId, user, - details.isDefaultValue || value == null, + details.isDefaultValue, details.error, - value ?: defaultValue, + details.value as T, details.fetchTimeUnixMilliseconds, - details.matchedEvaluationRule, - details.matchedEvaluationPercentageRule + details.matchedTargetingRule, + details.matchedPercentageOption ) } +@PublishedApi +internal suspend fun getValueDetailsInternal( + configCatClient: ConfigCatClient, + key: String, + defaultValue: Any?, + user: ConfigCatUser? +): EvaluationDetails { + val client = configCatClient as? Client + return client?.getValueDetailsImpl(key, defaultValue, user, allowAnyReturnType = false) + ?: configCatClient.getAnyValueDetails(key, defaultValue, user) +} + internal class Client private constructor( private val sdkKey: String, options: ConfigCatOptions @@ -231,6 +305,7 @@ internal class Client private constructor( private val service: ConfigService? private val flagOverrides: FlagOverrides? private val evaluator: Evaluator + private val logLevel: LogLevel private val logger: InternalLogger private var defaultUser: ConfigCatUser? private val isClosed = atomic(false) @@ -240,6 +315,7 @@ internal class Client private constructor( init { options.sdkKey = sdkKey logger = InternalLogger(options.logger, options.logLevel, options.hooks) + logLevel = options.logLevel hooks = options.hooks defaultUser = options.defaultUser flagOverrides = options.flagOverrides?.let { FlagOverrides().apply(it) } @@ -251,32 +327,84 @@ internal class Client private constructor( evaluator = Evaluator(logger) } - override suspend fun getAnyValue(key: String, defaultValue: Any, user: ConfigCatUser?): Any { + internal suspend fun getValueImpl( + key: String, + defaultValue: Any?, + user: ConfigCatUser?, + allowAnyReturnType: Boolean + ): Any? { + require(key.isNotEmpty()) { "'key' cannot be empty." } + val settingResult = getSettings() val evalUser = user ?: defaultUser - val checkSettingAvailableMessage = checkSettingAvailable(settingResult, key, defaultValue) - if (checkSettingAvailableMessage != null) { - val details = EvaluationDetails.makeError(key, defaultValue, checkSettingAvailableMessage, evalUser) + val checkSettingAvailable = checkSettingAvailable(settingResult, key, defaultValue) + val setting = checkSettingAvailable.second + if (setting == null) { + val details = EvaluationDetails.makeError(key, defaultValue, checkSettingAvailable.first, evalUser) hooks.invokeOnFlagEvaluated(details) return defaultValue } - val setting = settingResult.settings[key] + return try { + if (!allowAnyReturnType) { + validateValueType(setting.type, defaultValue) + } + evaluate(setting, key, evalUser, settingResult.fetchTime, settingResult.settings).value + } catch (exception: Exception) { + val errorMessage = ConfigCatLogMessages.getSettingEvaluationErrorWithDefaultValue( + "getAnyValue", + key, + "defaultValue", + defaultValue ?: "null" + ) + logger.error(1002, errorMessage, exception) + hooks.invokeOnFlagEvaluated(EvaluationDetails.makeError(key, defaultValue, errorMessage, evalUser)) + defaultValue + } + } - return evaluate(setting!!, key, evalUser, settingResult.fetchTime).value + override suspend fun getAnyValue(key: String, defaultValue: Any?, user: ConfigCatUser?): Any? { + return getValueImpl(key, defaultValue, user, allowAnyReturnType = true) } - override suspend fun getAnyValueDetails(key: String, defaultValue: Any, user: ConfigCatUser?): EvaluationDetails { + internal suspend fun getValueDetailsImpl( + key: String, + defaultValue: Any?, + user: ConfigCatUser?, + allowAnyReturnType: Boolean + ): EvaluationDetails { + require(key.isNotEmpty()) { "'key' cannot be empty." } + val settingResult = getSettings() val evalUser = user ?: defaultUser - val checkSettingAvailableMessage = checkSettingAvailable(settingResult, key, defaultValue) - if (checkSettingAvailableMessage != null) { - val details = EvaluationDetails.makeError(key, defaultValue, checkSettingAvailableMessage, evalUser) + + val checkSettingAvailable = checkSettingAvailable(settingResult, key, defaultValue) + val setting = checkSettingAvailable.second + if (setting == null) { + val details = EvaluationDetails.makeError(key, defaultValue, checkSettingAvailable.first, evalUser) hooks.invokeOnFlagEvaluated(details) return details } - val setting = settingResult.settings[key] + return try { + if (!allowAnyReturnType) { + validateValueType(setting.type, defaultValue) + } + evaluate(setting, key, evalUser, settingResult.fetchTime, settingResult.settings) + } catch (exception: Exception) { + val errorMessage = ConfigCatLogMessages.getSettingEvaluationErrorWithDefaultValue( + "getAnyValueDetails", + key, + "defaultValue", + defaultValue ?: "null" + ) + logger.error(1002, errorMessage, exception) + val errorDetails = EvaluationDetails.makeError(key, defaultValue, exception.message ?: "", evalUser) + hooks.invokeOnFlagEvaluated(errorDetails) + errorDetails + } + } - return evaluate(setting!!, key, evalUser, settingResult.fetchTime) + override suspend fun getAnyValueDetails(key: String, defaultValue: Any?, user: ConfigCatUser?): EvaluationDetails { + return getValueDetailsImpl(key, defaultValue, user, allowAnyReturnType = true) } override suspend fun getAllValueDetails(user: ConfigCatUser?): Collection { @@ -284,34 +412,71 @@ internal class Client private constructor( if (!checkSettingsAvailable(settingResult, "empty list")) { return emptyList() } - return settingResult.settings.map { - evaluate(it.value, it.key, user ?: defaultUser, settingResult.fetchTime) + return try { + settingResult.settings.map { + evaluate(it.value, it.key, user ?: defaultUser, settingResult.fetchTime, settingResult.settings) + } + } catch (exception: Exception) { + val errorMessage = + ConfigCatLogMessages.getSettingEvaluationErrorWithEmptyValue("getAllValueDetails", "empty list") + logger.error(1002, errorMessage, exception) + emptyList() } } override suspend fun getKeyAndValue(variationId: String): Pair? { - val settingResult = getSettings() - if (!checkSettingsAvailable(settingResult, "null")) { - return null - } - val settings = settingResult.settings - for (setting in settings) { - if (setting.value.variationId == variationId) { - return Pair(setting.key, setting.value.value) + require(variationId.isNotEmpty()) { "'variationId' cannot be empty." } + + try { + val settingResult = getSettings() + if (!checkSettingsAvailable(settingResult, "null")) { + return null } - for (rolloutRule in setting.value.rolloutRules) { - if (rolloutRule.variationId == variationId) { - return Pair(setting.key, rolloutRule.value) + val settings = settingResult.settings + for (setting in settings) { + if (setting.value.variationId == variationId) { + return Pair( + setting.key, + Helpers.validateSettingValueType(setting.value.settingValue, setting.value.type) + ) } - } - for (percentageRule in setting.value.percentageItems) { - if (percentageRule.variationId == variationId) { - return Pair(setting.key, percentageRule.value) + setting.value.targetingRules?.forEach { targetingRule -> + if (targetingRule.servedValue != null) { + if (targetingRule.servedValue.variationId == variationId) { + return Pair( + setting.key, + Helpers.validateSettingValueType(targetingRule.servedValue.value, setting.value.type) + ) + } + } else if (!targetingRule.percentageOptions.isNullOrEmpty()) { + targetingRule.percentageOptions.forEach { percentageOption -> + if (percentageOption.variationId == variationId) { + return Pair( + setting.key, + Helpers.validateSettingValueType(percentageOption.value, setting.value.type) + ) + } + } + } else { + error("Targeting rule THEN part is missing or invalid.") + } + } + setting.value.percentageOptions?.forEach { percentageOption -> + if (percentageOption.variationId == variationId) { + return Pair( + setting.key, + Helpers.validateSettingValueType(percentageOption.value, setting.value.type) + ) + } } } + this.logger.error(2011, ConfigCatLogMessages.getSettingForVariationIdIsNotPresent(variationId)) + return null + } catch (exception: Exception) { + val errorMessage = ConfigCatLogMessages.getSettingEvaluationErrorWithEmptyValue("getKeyAndValue", "null") + logger.error(1002, errorMessage, exception) + return null } - this.logger.error(2011, ConfigCatLogMessages.getSettingForVariationIdIsNotPresent(variationId)) - return null } override suspend fun getAllKeys(): Collection { @@ -322,15 +487,22 @@ internal class Client private constructor( return settingResult.settings.keys } - override suspend fun getAllValues(user: ConfigCatUser?): Map { + override suspend fun getAllValues(user: ConfigCatUser?): Map { val settingResult = getSettings() if (!checkSettingsAvailable(settingResult, "empty map")) { return emptyMap() } - return settingResult.settings.map { - val evaluated = evaluate(it.value, it.key, user ?: defaultUser, settingResult.fetchTime) - it.key to evaluated.value - }.toMap() + return try { + return settingResult.settings.map { + val evaluated = + evaluate(it.value, it.key, user ?: defaultUser, settingResult.fetchTime, settingResult.settings) + it.key to evaluated.value + }.toMap() + } catch (exception: Exception) { + val errorMessage = ConfigCatLogMessages.getSettingEvaluationErrorWithEmptyValue("getAllValues", "empty map") + logger.error(1002, errorMessage, exception) + emptyMap() + } } override suspend fun forceRefresh(): RefreshResult = service?.refresh() ?: RefreshResult( @@ -402,10 +574,26 @@ internal class Client private constructor( hooks.clear() } - private fun evaluate(setting: Setting, key: String, user: ConfigCatUser?, fetchTime: DateTime): EvaluationDetails { - val (value, variationId, targetingRule, percentageRule) = evaluator.evaluate(setting, key, user) + private fun evaluate( + setting: Setting, + key: String, + user: ConfigCatUser?, + fetchTime: DateTime, + settings: Map + ): EvaluationDetails { + var evaluateLogger: EvaluateLogger? = null + if (logLevel == LogLevel.INFO) { + evaluateLogger = EvaluateLogger() + } + val (value, variationId, targetingRule, percentageRule) = evaluator.evaluate( + setting, + key, + user, + settings, + evaluateLogger + ) val details = EvaluationDetails( - key, variationId, user, false, null, value, + key, variationId, user, false, null, Helpers.validateSettingValueType(value, setting.type), fetchTime.unixMillisLong, targetingRule, percentageRule ) hooks.invokeOnFlagEvaluated(details) @@ -439,6 +627,37 @@ internal class Client private constructor( return service?.getSettings() ?: SettingResult(mapOf(), Constants.distantPast) } + private fun validateValueType(settingTypeInt: Int, defaultValue: Any?) { + val settingType = settingTypeInt.toSettingTypeOrNull() + ?: throw IllegalArgumentException( + "The setting type is not valid. Only String, Int, Double or Boolean types are supported." + ) + if (defaultValue == null) { + return + } + if (!( + (defaultValue is String && settingType == SettingType.STRING) || + (defaultValue is Boolean && settingType == SettingType.BOOLEAN) || + ( + defaultValue is Int && (settingType == SettingType.INT || settingType == SettingType.JS_NUMBER) + ) || + ( + defaultValue is Double && ( + settingType == SettingType.DOUBLE || settingType == SettingType.JS_NUMBER + ) + ) + ) + ) { + throw IllegalArgumentException( + "The type of a setting must match the type of the specified default value. " + + "Setting's type was {" + settingType + "} but the default value's type was {" + + defaultValue::class.toString() + "}. Please use a default value which corresponds to the setting " + + "type {" + settingType + "}. Learn more: " + + "https://configcat.com/docs/sdk-reference/kotlin/#setting-type-mapping" + ) + } + } + private fun checkSettingsAvailable(settingResult: SettingResult, emptyResult: String): Boolean { if (settingResult.isEmpty()) { this.logger.error(1000, ConfigCatLogMessages.getConfigJsonIsNotPresentedWithEmptyResult(emptyResult)) @@ -451,12 +670,12 @@ internal class Client private constructor( settingResult: SettingResult, key: String, defaultValue: T - ): String? { + ): Pair { if (settingResult.isEmpty()) { val errorMessage = ConfigCatLogMessages.getConfigJsonIsNotPresentedWithDefaultValue(key, "defaultValue", defaultValue) logger.error(1000, errorMessage) - return errorMessage + return Pair(errorMessage, null) } val setting = settingResult.settings[key] if (setting == null) { @@ -467,9 +686,9 @@ internal class Client private constructor( settingResult.settings.keys ) logger.error(1001, errorMessage) - return errorMessage + return Pair(errorMessage, null) } - return null + return Pair("", setting) } companion object { @@ -477,19 +696,46 @@ internal class Client private constructor( private val lock = reentrantLock() fun get(sdkKey: String, block: ConfigCatOptions.() -> Unit = {}): Client { - require(sdkKey.isNotEmpty()) { "'sdkKey' cannot be empty." } + require(sdkKey.isNotEmpty()) { "SDK Key cannot be empty." } + val options = ConfigCatOptions().apply(block) + val flagOverrides = options.flagOverrides?.let { FlagOverrides().apply(it) } + if (OverrideBehavior.LOCAL_ONLY != flagOverrides?.behavior) { + require(isValidKey(sdkKey, options.isBaseURLCustom())) { "SDK Key '$sdkKey' is invalid." } + } + lock.withLock { val instance = instances[sdkKey] if (instance != null) { instance.logger.warning(3000, ConfigCatLogMessages.getClientIsAlreadyCreated(sdkKey)) return instance } - val client = Client(sdkKey, ConfigCatOptions().apply(block)) + val client = Client(sdkKey, options) instances[sdkKey] = client return client } } + private fun isValidKey(sdkKey: String, isCustomBaseURL: Boolean): Boolean { + // configcat-proxy/ rules + if (isCustomBaseURL && sdkKey.length > Constants.SDK_KEY_PROXY_PREFIX.length && + sdkKey.startsWith(Constants.SDK_KEY_PROXY_PREFIX) + ) { + return true + } + val splitSDKKey = sdkKey.split("/").toTypedArray() + // 22/22 rules + return if (splitSDKKey.size == 2 && splitSDKKey[0].length == Constants.SDK_KEY_SECTION_LENGTH && + splitSDKKey[1].length == Constants.SDK_KEY_SECTION_LENGTH + ) { + true + // configcat-sdk-1/22/22 rules + } else { + splitSDKKey.size == 3 && splitSDKKey[0] == Constants.SDK_KEY_PREFIX && + splitSDKKey[1].length == Constants.SDK_KEY_SECTION_LENGTH && + splitSDKKey[2].length == Constants.SDK_KEY_SECTION_LENGTH + } + } + fun removeFromInstances(client: Client) { lock.withLock { if (instances[client.sdkKey] == client) { @@ -507,4 +753,8 @@ internal class Client private constructor( } } } + + internal object SettingTypeHelper { + fun Int.toSettingTypeOrNull(): SettingType? = SettingType.values().firstOrNull { it.id == this } + } } diff --git a/src/commonMain/kotlin/com/configcat/ConfigCatUser.kt b/src/commonMain/kotlin/com/configcat/ConfigCatUser.kt index 735272c2..6f6b4ec3 100644 --- a/src/commonMain/kotlin/com/configcat/ConfigCatUser.kt +++ b/src/commonMain/kotlin/com/configcat/ConfigCatUser.kt @@ -1,34 +1,81 @@ package com.configcat +import kotlinx.serialization.encodeToString + /** * An object containing attributes to properly identify a given user for variation evaluation. * Its only mandatory attribute is the [identifier]. + * + * Custom attributes of the user for advanced targeting rule definitions (e.g. user role, subscription type, etc.) + * + * The set of allowed attribute values depends on the comparison type of the condition which references the + * User Object attribute.
+ * [String] values are supported by all comparison types (in some cases they need to be provided in a specific + * format though).
+ * Some of the comparison types work with other types of values, as described below. + * + * Text-based comparisons (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 comparisons (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 comparisons (=, <, >=, etc.)
+ * * accept [Double] values and all other numeric values which can safely be converted to [Double] + * * accept [String] values containing a properly formatted, valid [Double] value + * * all other values are considered invalid (a warning will be logged and the currently evaluated targeting rule + * will be skipped). + * + * Date time-based comparisons (BEFORE / AFTER)
+ * * accept [com.soywiz.klock.DateTime] values, which are automatically converted to a second-based Unix timestamp + * * accept [Double] values representing a second-based Unix timestamp and all other numeric values which can safely + * be converted to {@link Double} + * * accept [String] values containing a properly formatted, valid [Double] value + * * all other values are considered invalid (a warning will be logged and the currently evaluated targeting rule will + * be skipped). + * + * String array-based comparisons (ARRAY CONTAINS ANY OF / ARRAY NOT CONTAINS ANY OF)
+ * * accept arrays of [String] + * * accept [List] 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). + * + * In case a non-string attribute value needs to be converted to [String] during evaluation, it will always be done + * using the same format which is accepted by the comparisons. */ public class ConfigCatUser( public val identifier: String, email: String? = null, country: String? = null, - custom: Map? = null + custom: Map? = null ) { - private val attributes: Map + private val attributes: Map init { - val attr = mutableMapOf("Identifier" to identifier) - if (email != null) { + val attr = mutableMapOf() + attr["Identifier"] = identifier + if (!email.isNullOrEmpty()) { attr["Email"] = email } - if (country != null) { + if (!country.isNullOrEmpty()) { attr["Country"] = country } if (custom != null) { for (item in custom) { - attr[item.key] = item.value + if (item.key != "Identifier" && item.key != "Email" && item.key != "Country") { + attr[item.key] = item.value + } } } attributes = attr } - internal fun attributeFor(key: String): String? { + internal fun attributeFor(key: String): Any? { if (key.isEmpty()) { return null } @@ -36,6 +83,6 @@ public class ConfigCatUser( } override fun toString(): String { - return "{${attributes.map { "${it.key}: ${it.value}" }.joinToString()}}" + return Constants.json.encodeToString(attributes) } } diff --git a/src/commonMain/kotlin/com/configcat/ConfigService.kt b/src/commonMain/kotlin/com/configcat/ConfigService.kt index d1f0d158..7eaf1197 100644 --- a/src/commonMain/kotlin/com/configcat/ConfigService.kt +++ b/src/commonMain/kotlin/com/configcat/ConfigService.kt @@ -4,6 +4,8 @@ import com.configcat.fetch.ConfigFetcher import com.configcat.fetch.RefreshResult import com.configcat.log.ConfigCatLogMessages import com.configcat.log.InternalLogger +import com.configcat.model.Entry +import com.configcat.model.Setting import com.soywiz.klock.DateTime import com.soywiz.krypto.sha1 import io.ktor.util.* @@ -22,7 +24,7 @@ internal data class SettingResult(val settings: Map, val fetchT } } -internal class ConfigService constructor( +internal class ConfigService( private val options: ConfigCatOptions, private val configFetcher: ConfigFetcher, private val logger: InternalLogger, @@ -62,16 +64,17 @@ internal class ConfigService constructor( if (result.first.isEmpty()) { SettingResult.empty } else { - SettingResult(result.first.config.settings, result.first.fetchTime) + SettingResult(result.first.config.settings ?: emptyMap(), result.first.fetchTime) } } else -> { - val result = fetchIfOlder(Constants.distantPast, preferCached = initialized.value) // If we are initialized, we prefer the cached results + // If we are initialized, we prefer the cached results + val result = fetchIfOlder(Constants.distantPast, preferCached = initialized.value) if (result.first.isEmpty()) { SettingResult.empty } else { - SettingResult(result.first.config.settings, result.first.fetchTime) + SettingResult(result.first.config.settings ?: emptyMap(), result.first.fetchTime) } } } diff --git a/src/commonMain/kotlin/com/configcat/Constants.kt b/src/commonMain/kotlin/com/configcat/Constants.kt new file mode 100644 index 00000000..34a66990 --- /dev/null +++ b/src/commonMain/kotlin/com/configcat/Constants.kt @@ -0,0 +1,123 @@ +package com.configcat + +import com.configcat.Client.SettingTypeHelper.toSettingTypeOrNull +import com.configcat.model.Config +import com.configcat.model.SettingType +import com.configcat.model.SettingValue +import com.soywiz.klock.DateTime +import kotlinx.serialization.ContextualSerializer +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.* +import kotlinx.serialization.modules.SerializersModule + +internal interface Closeable { + fun close() +} + +internal object Constants { + const val version: String = "3.0.0" + const val configFileName: String = "config_v6.json" + const val serializationFormatVersion: String = "v2" + const val globalCdnUrl = "https://cdn-global.configcat.com" + const val euCdnUrl = "https://cdn-eu.configcat.com" + const val SDK_KEY_PROXY_PREFIX = "configcat-proxy/" + const val SDK_KEY_PREFIX = "configcat-sdk-1" + const val SDK_KEY_SECTION_LENGTH = 22 + + val distantPast = DateTime.fromUnix(0) + val distantFuture = DateTime.now().add(10_000, 0.0) + val json = Json { + ignoreUnknownKeys = true + serializersModule = SerializersModule { + contextual(Any::class, FlagValueSerializer) + } + } + + internal object FlagValueSerializer : KSerializer { + override fun deserialize(decoder: Decoder): Any { + val json = decoder as? JsonDecoder + ?: error("Only JsonDecoder is supported.") + val element = json.decodeJsonElement() + val primitive = element as? JsonPrimitive ?: error("Unable to decode $element") + return when (primitive.content) { + "true", "false" -> primitive.content == "true" + else -> primitive.content.toIntOrNull() ?: primitive.content.toDoubleOrNull() ?: primitive.content + } + } + + override fun serialize(encoder: Encoder, value: Any) { + val json = encoder as? JsonEncoder + ?: error("Only JsonEncoder is supported.") + val element: JsonElement = when (value) { + is String -> JsonPrimitive(value) + is Number -> JsonPrimitive(value) + is Boolean -> JsonPrimitive(value) + is JsonElement -> value + else -> throw IllegalArgumentException("Unable to encode $value") + } + json.encodeJsonElement(element) + } + + @OptIn(ExperimentalSerializationApi::class) + override val descriptor: SerialDescriptor = + ContextualSerializer(Any::class, null, emptyArray()).descriptor + } +} + +internal object Helpers { + fun parseConfigJson(jsonString: String): Config { + val config: Config = Constants.json.decodeFromString(jsonString) + addConfigSaltAndSegmentsToSettings(config) + return config + } + + fun addConfigSaltAndSegmentsToSettings(config: Config) { + val configSalt = config.preferences?.salt + config.settings?.values?.forEach { + it.configSalt = configSalt + it.segments = config.segments ?: arrayOf() + } + } + + fun validateSettingValueType(settingValue: SettingValue?, settingType: Int): Any { + val settingTypeEnum = settingType.toSettingTypeOrNull() + require(settingValue != null) { "Setting value is missing or invalid." } + val result: Any? + result = when (settingTypeEnum) { + SettingType.BOOLEAN -> { + settingValue.booleanValue + } + + SettingType.STRING -> { + settingValue.stringValue + } + + SettingType.INT -> { + settingValue.integerValue + } + + SettingType.DOUBLE -> { + settingValue.doubleValue + } + + SettingType.JS_NUMBER -> { + settingValue.doubleValue + } + + else -> { + throw IllegalArgumentException( + "Setting is of an unsupported type ($settingTypeEnum)." + ) + } + } + require(result != null) { + "Setting value is not of the expected type ${settingTypeEnum.value}." + } + return result + } +} diff --git a/src/commonMain/kotlin/com/configcat/DateTimeUtils.kt b/src/commonMain/kotlin/com/configcat/DateTimeUtils.kt index e3232c99..6f03c637 100644 --- a/src/commonMain/kotlin/com/configcat/DateTimeUtils.kt +++ b/src/commonMain/kotlin/com/configcat/DateTimeUtils.kt @@ -1,8 +1,16 @@ package com.configcat +import com.soywiz.klock.DateTime + internal object DateTimeUtils { fun isValidDate(fetchTime: String): Boolean { fetchTime.toLongOrNull() ?: return false return true } + + fun Double.toDateTimeUTCString(): String { + val dateInMillisecond: Long = this.toLong() * 1000 + val dateTime = DateTime.fromUnix(dateInMillisecond) + return dateTime.toString("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") + } } diff --git a/src/commonMain/kotlin/com/configcat/EvaluateLogger.kt b/src/commonMain/kotlin/com/configcat/EvaluateLogger.kt new file mode 100644 index 00000000..63bdc73e --- /dev/null +++ b/src/commonMain/kotlin/com/configcat/EvaluateLogger.kt @@ -0,0 +1,346 @@ +package com.configcat + +import com.configcat.ComparatorHelp.toComparatorOrNull +import com.configcat.ComparatorHelp.toPrerequisiteComparatorOrNull +import com.configcat.ComparatorHelp.toSegmentComparatorOrNull +import com.configcat.DateTimeUtils.toDateTimeUTCString +import com.configcat.model.* + +@Suppress("TooManyFunctions") +internal class EvaluateLogger { + private val entries = StringBuilder() + private var indentLevel: Int = 0 + + fun append(line: String) { + entries.append(line) + } + + fun increaseIndentLevel() { + indentLevel++ + } + + fun decreaseIndentLevel() { + if (indentLevel > 0) { + indentLevel-- + } + } + + fun newLine() { + entries.appendLine() + for (i in 0 until indentLevel) { + entries.append(" ") + } + } + + fun print(): String { + return entries.toString() + } + + fun logEvaluation(key: String) { + append("Evaluating '$key'") + } + + fun logReturnValue(value: Any) { + newLine() + append("Returning '$value'.") + } + + fun logUserObject(user: ConfigCatUser) { + append(" for User '$user'") + } + + fun logPercentageOptionUserMissing() { + newLine() + append("Skipping % options because the User Object is missing.") + } + + fun logPercentageOptionUserAttributeMissing(percentageOptionsAttributeName: String) { + newLine() + append("Skipping % options because the User.$percentageOptionsAttributeName attribute is missing.") + } + + fun logPercentageOptionEvaluation(percentageOptionsAttributeName: String) { + newLine() + append("Evaluating % options based on the User.$percentageOptionsAttributeName attribute:") + } + + fun logPercentageOptionEvaluationHash(percentageOptionsAttributeName: String, hashValue: Int) { + newLine() + append( + "- Computing hash in the [0..99] range from User.$percentageOptionsAttributeName => " + + "$hashValue (this value is sticky and consistent across all SDKs)" + ) + } + + fun logTargetingRules() { + newLine() + append("Evaluating targeting rules and applying the first match if any:") + } + + fun logConditionConsequence(result: Boolean) { + append(" => $result") + if (!result) { + append(", skipping the remaining AND conditions") + } + } + + fun logTargetingRuleIgnored() { + increaseIndentLevel() + newLine() + append("The current targeting rule is ignored and the evaluation continues with the next rule.") + decreaseIndentLevel() + } + + fun logTargetingRuleConsequence(targetingRule: TargetingRule?, error: String?, isMatch: Boolean, newLine: Boolean) { + increaseIndentLevel() + var valueFormat = "% options" + if (targetingRule?.servedValue?.value != null) { + valueFormat = "'" + targetingRule.servedValue.value + "'" + } + if (newLine) { + newLine() + } else { + append(" ") + } + append("THEN $valueFormat => ") + if (!error.isNullOrEmpty()) { + append(error) + } else { + if (isMatch) { + append("MATCH, applying rule") + } else { + append("no match") + } + } + decreaseIndentLevel() + } + + fun logPercentageEvaluationReturnValue(hashValue: Int, i: Int, percentage: Int, settingValue: SettingValue?) { + val percentageOptionValue = settingValue?.toString() ?: EvaluatorLogHelper.INVALID_VALUE + newLine() + append("- Hash value $hashValue selects % option ${(i + 1)} ($percentage%), '$percentageOptionValue'.") + } + + fun logSegmentEvaluationStart(segmentName: String) { + newLine() + append("(") + increaseIndentLevel() + newLine() + append("Evaluating segment '$segmentName':") + } + + fun logSegmentEvaluationResult( + segmentCondition: SegmentCondition?, + segment: Segment?, + result: Boolean, + segmentResult: Boolean + ) { + newLine() + val segmentResultComparator: String = + if (segmentResult) { + Evaluator.SegmentComparator.IS_IN_SEGMENT.value + } else { + Evaluator.SegmentComparator.IS_NOT_IN_SEGMENT.value + } + append("Segment evaluation result: User $segmentResultComparator.") + newLine() + append( + "Condition (${EvaluatorLogHelper.formatSegmentFlagCondition(segmentCondition, segment)}) evaluates" + + " to $result." + ) + decreaseIndentLevel() + newLine() + append(")") + } + + fun logSegmentEvaluationError(segmentCondition: SegmentCondition?, segment: Segment?, error: String?) { + newLine() + append("Segment evaluation result: $error.") + newLine() + append( + "Condition (${EvaluatorLogHelper.formatSegmentFlagCondition(segmentCondition, segment)}) failed to " + + "evaluate." + ) + decreaseIndentLevel() + newLine() + append(")") + } + + fun logPrerequisiteFlagEvaluationStart(prerequisiteFlagKey: String) { + newLine() + append("(") + increaseIndentLevel() + newLine() + append("Evaluating prerequisite flag '$prerequisiteFlagKey':") + } + + fun logPrerequisiteFlagEvaluationResult( + prerequisiteFlagCondition: PrerequisiteFlagCondition?, + prerequisiteFlagValue: SettingValue?, + result: Boolean + ) { + newLine() + val prerequisiteFlagValueFormat = prerequisiteFlagValue?.toString() ?: EvaluatorLogHelper.INVALID_VALUE + append("Prerequisite flag evaluation result: '$prerequisiteFlagValueFormat'.") + newLine() + append( + "Condition (${EvaluatorLogHelper.formatPrerequisiteFlagCondition(prerequisiteFlagCondition!!)}) " + + "evaluates to $result." + ) + decreaseIndentLevel() + newLine() + append(")") + } +} + +internal object EvaluatorLogHelper { + private const val HASHED_VALUE = "" + const val INVALID_VALUE = "" + private const val INVALID_NAME = "" + private const val INVALID_REFERENCE = "" + private const val MAX_LIST_ELEMENT = 10 + private fun formatStringListComparisonValue(comparisonValue: Array?, isSensitive: Boolean): String { + if (comparisonValue == null) { + return INVALID_VALUE + } + val comparisonValues = comparisonValue.map { it } + if (comparisonValues.isEmpty()) { + return INVALID_VALUE + } + val formattedList: String + if (isSensitive) { + val sensitivePostFix = if (comparisonValues.size == 1) "value" else "values" + formattedList = "<${comparisonValues.size} hashed $sensitivePostFix>" + } else { + var listPostFix = "" + if (comparisonValues.size > MAX_LIST_ELEMENT) { + val count = comparisonValues.size - MAX_LIST_ELEMENT + val countPostFix = if (count == 1) "value" else "values" + listPostFix = ", ... <$count more $countPostFix>" + } + val subList: List = comparisonValues.subList(0, minOf(MAX_LIST_ELEMENT, comparisonValues.size)) + val formatListBuilder = StringBuilder() + for (i in subList.indices) { + formatListBuilder.append("'${subList[i]}'") + if (i != subList.size - 1) { + formatListBuilder.append(", ") + } + } + formatListBuilder.append(listPostFix) + formattedList = formatListBuilder.toString() + } + return "[$formattedList]" + } + + private fun formatStringComparisonValue(comparisonValue: String?, isSensitive: Boolean): String { + return if (isSensitive) { + "'$HASHED_VALUE'" + } else { + "'$comparisonValue'" + } + } + + private fun formatDoubleComparisonValue(comparisonValue: Double?, isDate: Boolean): String { + if (comparisonValue == null) { + return INVALID_VALUE + } + val comparisonValueString = formatDoubleForLog(comparisonValue) + return if (isDate) { + "'$comparisonValueString' (${comparisonValue.toDateTimeUTCString()} UTC)" + } else { + "'$comparisonValueString'" + } + } + + fun formatUserCondition(userCondition: UserCondition): String { + val userComparator = userCondition.comparator.toComparatorOrNull() + val comparisonValue: String = when (userComparator) { + Evaluator.UserComparator.IS_ONE_OF, + Evaluator.UserComparator.IS_NOT_ONE_OF, + Evaluator.UserComparator.CONTAINS_ANY_OF, + Evaluator.UserComparator.NOT_CONTAINS_ANY_OF, + Evaluator.UserComparator.ONE_OF_SEMVER, + Evaluator.UserComparator.NOT_ONE_OF_SEMVER, + Evaluator.UserComparator.TEXT_STARTS_WITH, + Evaluator.UserComparator.TEXT_NOT_STARTS_WITH, + Evaluator.UserComparator.TEXT_ENDS_WITH, + Evaluator.UserComparator.TEXT_NOT_ENDS_WITH, + Evaluator.UserComparator.TEXT_ARRAY_CONTAINS, + Evaluator.UserComparator.TEXT_ARRAY_NOT_CONTAINS -> formatStringListComparisonValue( + userCondition.stringArrayValue, + false + ) + + Evaluator.UserComparator.LT_SEMVER, + Evaluator.UserComparator.LTE_SEMVER, + Evaluator.UserComparator.GT_SEMVER, + Evaluator.UserComparator.GTE_SEMVER, + Evaluator.UserComparator.TEXT_EQUALS, + Evaluator.UserComparator.TEXT_NOT_EQUALS -> formatStringComparisonValue( + userCondition.stringValue, + false + ) + + Evaluator.UserComparator.EQ_NUM, + Evaluator.UserComparator.NOT_EQ_NUM, + Evaluator.UserComparator.LT_NUM, + Evaluator.UserComparator.LTE_NUM, + Evaluator.UserComparator.GT_NUM, + Evaluator.UserComparator.GTE_NUM -> formatDoubleComparisonValue( + userCondition.doubleValue, + false + ) + + Evaluator.UserComparator.ONE_OF_SENS, + Evaluator.UserComparator.NOT_ONE_OF_SENS, + Evaluator.UserComparator.HASHED_STARTS_WITH, + Evaluator.UserComparator.HASHED_NOT_STARTS_WITH, + Evaluator.UserComparator.HASHED_ENDS_WITH, + Evaluator.UserComparator.HASHED_NOT_ENDS_WITH, + Evaluator.UserComparator.HASHED_ARRAY_CONTAINS, + Evaluator.UserComparator.HASHED_ARRAY_NOT_CONTAINS -> formatStringListComparisonValue( + userCondition.stringArrayValue, + true + ) + + Evaluator.UserComparator.DATE_BEFORE, Evaluator.UserComparator.DATE_AFTER -> formatDoubleComparisonValue( + userCondition.doubleValue, + true + ) + + Evaluator.UserComparator.HASHED_EQUALS, Evaluator.UserComparator.HASHED_NOT_EQUALS -> + formatStringComparisonValue( + userCondition.stringValue, + true + ) + + else -> INVALID_VALUE + } + return "User.${userCondition.comparisonAttribute} ${userComparator?.value} $comparisonValue" + } + + fun formatPrerequisiteFlagCondition(prerequisiteFlagCondition: PrerequisiteFlagCondition): String { + val prerequisiteComparator = prerequisiteFlagCondition.prerequisiteComparator.toPrerequisiteComparatorOrNull() + return "Flag '${prerequisiteFlagCondition.prerequisiteFlagKey}' ${prerequisiteComparator?.value} " + + "'${prerequisiteFlagCondition.value ?: INVALID_VALUE}'" + } + + fun formatCircularDependencyList(visitedKeys: List, key: String?): String { + val builder = StringBuilder() + visitedKeys.forEach { visitedKey: String? -> + builder.append("'").append(visitedKey).append("' -> ") + } + builder.append("'").append(key).append("'") + return builder.toString() + } + + fun formatSegmentFlagCondition(segmentCondition: SegmentCondition?, segment: Segment?): String { + val segmentName: String = if (segment != null) { + segment.name ?: INVALID_NAME + } else { + INVALID_REFERENCE + } + val prerequisiteComparator = segmentCondition?.segmentComparator?.toSegmentComparatorOrNull() + return "User ${prerequisiteComparator?.value} '$segmentName'" + } +} diff --git a/src/commonMain/kotlin/com/configcat/EvaluationDetails.kt b/src/commonMain/kotlin/com/configcat/EvaluationDetails.kt index 6d42bff8..a96b8c79 100644 --- a/src/commonMain/kotlin/com/configcat/EvaluationDetails.kt +++ b/src/commonMain/kotlin/com/configcat/EvaluationDetails.kt @@ -1,5 +1,8 @@ package com.configcat +import com.configcat.model.PercentageOption +import com.configcat.model.TargetingRule + /** * Additional information about flag evaluation. */ @@ -10,8 +13,8 @@ public open class EvaluationDetailsBase internal constructor( public val isDefaultValue: Boolean, public val error: String?, public val fetchTimeUnixMilliseconds: Long, - public val matchedEvaluationRule: RolloutRule?, - public val matchedEvaluationPercentageRule: PercentageRule? + public val matchedTargetingRule: TargetingRule?, + public val matchedPercentageOption: PercentageOption? ) /** @@ -25,8 +28,8 @@ public class TypedEvaluationDetails public constructor( error: String?, public val value: T, fetchTimeUnixMilliseconds: Long, - matchedEvaluationRule: RolloutRule?, - matchedEvaluationPercentageRule: PercentageRule? + matchedTargetingRule: TargetingRule?, + matchedPercentageOption: PercentageOption? ) : EvaluationDetailsBase( key, variationId, @@ -34,8 +37,8 @@ public class TypedEvaluationDetails public constructor( isDefaultValue, error, fetchTimeUnixMilliseconds, - matchedEvaluationRule, - matchedEvaluationPercentageRule + matchedTargetingRule, + matchedPercentageOption ) /** @@ -47,10 +50,10 @@ public class EvaluationDetails internal constructor( user: ConfigCatUser?, isDefaultValue: Boolean, error: String?, - public val value: Any, + public val value: Any?, fetchTimeUnixMilliseconds: Long, - matchedEvaluationRule: RolloutRule?, - matchedEvaluationPercentageRule: PercentageRule? + matchedTargetingRule: TargetingRule?, + matchedPercentageOption: PercentageOption? ) : EvaluationDetailsBase( key, variationId, @@ -58,11 +61,11 @@ public class EvaluationDetails internal constructor( isDefaultValue, error, fetchTimeUnixMilliseconds, - matchedEvaluationRule, - matchedEvaluationPercentageRule + matchedTargetingRule, + matchedPercentageOption ) { internal companion object { - internal fun makeError(key: String, defaultValue: Any, error: String, user: ConfigCatUser?): + internal fun makeError(key: String, defaultValue: Any?, error: String, user: ConfigCatUser?): EvaluationDetails = EvaluationDetails( key, "", user, true, error, defaultValue, Constants.distantPast.unixMillisLong, null, null diff --git a/src/commonMain/kotlin/com/configcat/Evaluator.kt b/src/commonMain/kotlin/com/configcat/Evaluator.kt index f0c14ea3..bf79c477 100644 --- a/src/commonMain/kotlin/com/configcat/Evaluator.kt +++ b/src/commonMain/kotlin/com/configcat/Evaluator.kt @@ -1,437 +1,1175 @@ package com.configcat +import com.configcat.Client.SettingTypeHelper.toSettingTypeOrNull +import com.configcat.ComparatorHelp.toComparatorOrNull +import com.configcat.ComparatorHelp.toPrerequisiteComparatorOrNull +import com.configcat.ComparatorHelp.toSegmentComparatorOrNull import com.configcat.log.ConfigCatLogMessages import com.configcat.log.InternalLogger +import com.configcat.model.* +import com.soywiz.klock.DateTime +import com.soywiz.klock.DateTimeTz import com.soywiz.krypto.sha1 +import com.soywiz.krypto.sha256 +import io.github.z4kn4fein.semver.Version import io.github.z4kn4fein.semver.VersionFormatException import io.github.z4kn4fein.semver.toVersion +import io.github.z4kn4fein.semver.toVersionOrNull +import io.ktor.http.* +import io.ktor.utils.io.charsets.* +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlin.math.absoluteValue internal data class EvaluationResult( - val value: Any, + val value: SettingValue, val variationId: String?, - val targetingRule: RolloutRule? = null, - val percentageRule: PercentageRule? = null + val matchedTargetingRule: TargetingRule? = null, + val matchedPercentageOption: PercentageOption? = null ) -internal class Evaluator(private val logger: InternalLogger) { +internal data class EvaluationContext( + val key: String, + val user: ConfigCatUser?, + val visitedKeys: ArrayList?, + val settings: Map?, + var isUserMissing: Boolean = false, + var isUserAttributeMissing: Boolean = false +) + +internal object ComparatorHelp { + fun Int.toComparatorOrNull(): Evaluator.UserComparator? = + Evaluator.UserComparator.values().firstOrNull { it.id == this } - // evaluatorLogger: EvaluatorLogger; + fun Int.toPrerequisiteComparatorOrNull(): Evaluator.PrerequisiteComparator? = + Evaluator.PrerequisiteComparator.values().firstOrNull { it.id == this } + + fun Int.toSegmentComparatorOrNull(): Evaluator.SegmentComparator? = + Evaluator.SegmentComparator.values().firstOrNull { it.id == this } +} - fun evaluate(setting: Setting, key: String, user: ConfigCatUser?): EvaluationResult { - val evaluatorLogger = EvaluatorLogger(key) +private const val USER_OBJECT_IS_MISSING = "cannot evaluate, User Object is missing" + +@Suppress("LargeClass", "TooManyFunctions") +internal class Evaluator(private val logger: InternalLogger) { + + fun evaluate( + setting: Setting, + key: String, + user: ConfigCatUser?, + settings: Map?, + evaluateLogger: EvaluateLogger? + ): EvaluationResult { try { - if (user == null) { - if (setting.rolloutRules.isNotEmpty() || setting.percentageItems.isNotEmpty()) { - logger.warning(3001, ConfigCatLogMessages.getTargetingIsNotPossible(key)) - } - evaluatorLogger.logReturnValue(setting.value) - return EvaluationResult(setting.value, setting.variationId) + evaluateLogger?.logEvaluation(key) + if (user != null) { + evaluateLogger?.logUserObject(user) } - evaluatorLogger.logUserObject(user) - val valueFromTargetingRules = processTargetingRules(setting, user, evaluatorLogger) - if (valueFromTargetingRules != null) return valueFromTargetingRules + evaluateLogger?.increaseIndentLevel() - val valueFromPercentageRules = processPercentageRules(setting, user, key, evaluatorLogger) - if (valueFromPercentageRules != null) return valueFromPercentageRules + val context = EvaluationContext(key, user, null, settings) - evaluatorLogger.logReturnValue(setting.value) - return EvaluationResult(setting.value, setting.variationId) + val evaluationResult = evaluateSetting(setting, evaluateLogger, context) + + evaluateLogger?.logReturnValue(evaluationResult.value) + evaluateLogger?.decreaseIndentLevel() + return evaluationResult } finally { - logger.info(5000, evaluatorLogger.print()) + if (evaluateLogger != null) { + logger.info(5000, evaluateLogger.print()) + } } } - @Suppress("ComplexMethod", "LoopWithTooManyJumpStatements") - private fun processTargetingRules( + private fun evaluateSetting( setting: Setting, - user: ConfigCatUser, - evaluatorLogger: EvaluatorLogger - ): EvaluationResult? { - if (setting.rolloutRules.isEmpty()) { - return null + evaluateLogger: EvaluateLogger?, + context: EvaluationContext + ): EvaluationResult { + var evaluationResult: EvaluationResult? = null + if (!setting.targetingRules.isNullOrEmpty()) { + evaluationResult = evaluateTargetingRules(setting.targetingRules, setting, context, evaluateLogger) + } + if (evaluationResult == null && !setting.percentageOptions.isNullOrEmpty()) { + evaluationResult = evaluatePercentageOptions( + setting.percentageOptions, + setting.percentageAttribute, + context, + null, + evaluateLogger + ) } - for (rule in setting.rolloutRules) { - val userValue = user.attributeFor(rule.comparisonAttribute) - val comparator = rule.comparator.toComparatorOrNull() + if (evaluationResult == null) { + evaluationResult = EvaluationResult(setting.settingValue, setting.variationId) + } + return evaluationResult + } - if (comparator == null) { - evaluatorLogger.logComparatorError( - rule.comparisonAttribute, - userValue ?: "", - rule.comparator, - rule.comparisonValue + @Suppress("LoopWithTooManyJumpStatements", "CyclomaticComplexMethod") + private fun evaluateTargetingRules( + targetingRules: Array, + setting: Setting, + context: EvaluationContext, + evaluateLogger: EvaluateLogger? + ): EvaluationResult? { + evaluateLogger?.logTargetingRules() + for (rule: TargetingRule in targetingRules) { + var evaluateConditionsResult: Boolean + var error: String? = null + try { + evaluateConditionsResult = evaluateConditions( + rule.conditionAccessors, + rule, + setting.configSalt, + context.key, + context, + setting.segments, + evaluateLogger ) + } catch (rolloutEvaluatorException: RolloutEvaluatorException) { + error = rolloutEvaluatorException.message + evaluateConditionsResult = false + } + if (!evaluateConditionsResult) { + if (error != null) { + evaluateLogger?.logTargetingRuleIgnored() + } continue } - if (userValue.isNullOrEmpty() || rule.comparisonValue.isEmpty()) { - evaluatorLogger.logNoMatch( - rule.comparisonAttribute, - userValue ?: "", - comparator, - rule.comparisonValue - ) + + if (rule.servedValue != null) { + return EvaluationResult(rule.servedValue.value, rule.servedValue.variationId, rule, null) + } + if (rule.percentageOptions.isNullOrEmpty()) { + error("Targeting rule THEN part is missing or invalid.") + } + evaluateLogger?.increaseIndentLevel() + val evaluatePercentageOptions = evaluatePercentageOptions( + rule.percentageOptions, + setting.percentageAttribute, + context, + rule, + evaluateLogger + ) + evaluateLogger?.decreaseIndentLevel() + + if (evaluatePercentageOptions == null) { + evaluateLogger?.logTargetingRuleIgnored() continue } + return evaluatePercentageOptions + } + return null + } - when (comparator) { - Comparator.ONE_OF, - Comparator.NOT_ONE_OF -> { - val value = processOneOf(rule, userValue, evaluatorLogger, comparator) - if (value != null) return value + @Suppress("NestedBlockDepth", "CyclomaticComplexMethod", "LongMethod", "LongParameterList") + private fun evaluateConditions( + conditions: List, + targetingRule: TargetingRule?, + configSalt: String?, + contextSalt: String, + context: EvaluationContext, + segments: Array, + evaluateLogger: EvaluateLogger? + ): Boolean { + var conditionsEvaluationResult = true + var error: String? = null + var newLine = false + for ((index, condition) in conditions.withIndex()) { + when (index) { + 0 -> { + evaluateLogger?.newLine() + evaluateLogger?.append("- IF ") + evaluateLogger?.increaseIndentLevel() } - Comparator.CONTAINS, - Comparator.NOT_CONTAINS -> { - val value = processContains(rule, userValue, evaluatorLogger, comparator) - if (value != null) return value + else -> { + evaluateLogger?.increaseIndentLevel() + evaluateLogger?.newLine() + evaluateLogger?.append("AND ") } + } - Comparator.ONE_OF_SEMVER, - Comparator.NOT_ONE_OF_SEMVER -> { - val value = processSemverOneOf(rule, userValue, evaluatorLogger, comparator) - if (value != null) return value + condition.userCondition?.let { userCondition -> + try { + conditionsEvaluationResult = evaluateUserCondition( + userCondition, + configSalt, + context, + contextSalt, + evaluateLogger + ) + } catch (evaluatorException: RolloutEvaluatorException) { + error = evaluatorException.message + conditionsEvaluationResult = false } + newLine = conditions.size > 1 + } - Comparator.LT_SEMVER, - Comparator.LTE_SEMVER, - Comparator.GT_SEMVER, - Comparator.GTE_SEMVER -> { - val value = processSemverCompare(rule, userValue, evaluatorLogger, comparator) - if (value != null) return value + condition.segmentCondition?.let { segmentCondition -> + try { + conditionsEvaluationResult = evaluateSegmentCondition( + segmentCondition, + context, + configSalt, + segments, + evaluateLogger + ) + } catch (evaluatorException: RolloutEvaluatorException) { + error = evaluatorException.message + conditionsEvaluationResult = false } + newLine = error == null || USER_OBJECT_IS_MISSING != error || conditions.size > 1 + } - Comparator.EQ_NUM, - Comparator.NOT_EQ_NUM, - Comparator.LT_NUM, - Comparator.LTE_NUM, - Comparator.GT_NUM, - Comparator.GTE_NUM -> { - val value = processNumber(rule, userValue, evaluatorLogger, comparator) - if (value != null) return value + condition.prerequisiteFlagCondition?.let { prerequisiteCondition -> + try { + conditionsEvaluationResult = evaluatePrerequisiteFlagCondition( + prerequisiteCondition, + context, + evaluateLogger + ) + } catch (evaluatorException: RolloutEvaluatorException) { + error = evaluatorException.message + conditionsEvaluationResult = false } + newLine = true + } - Comparator.ONE_OF_SENS, - Comparator.NOT_ONE_OF_SENS -> { - val value = processSensitiveOneOf(rule, userValue, evaluatorLogger, comparator) - if (value != null) return value - } + if (targetingRule == null || conditions.size > 1) { + evaluateLogger?.logConditionConsequence(conditionsEvaluationResult) + } + evaluateLogger?.decreaseIndentLevel() + if (!conditionsEvaluationResult) { + break } } - return null + targetingRule?.let { + evaluateLogger?.logTargetingRuleConsequence(targetingRule, error, conditionsEvaluationResult, newLine) + } + error?.let { + throw RolloutEvaluatorException(error) + } + return conditionsEvaluationResult } - private fun processOneOf( - rule: RolloutRule, - userValue: String, - evaluatorLogger: EvaluatorLogger, - comparator: Comparator - ): EvaluationResult? { - val split = rule.comparisonValue.split(",").map { it.trim() }.filter { it.isNotEmpty() } - val matchCondition = when (comparator) { - Comparator.ONE_OF -> split.contains(userValue) - Comparator.NOT_ONE_OF -> !split.contains(userValue) - else -> false - } - if (matchCondition) { - evaluatorLogger.logMatch( - rule.comparisonAttribute, - userValue, - comparator, - rule.comparisonValue, - rule.value + @Suppress("ThrowsCount") + private fun evaluateSegmentCondition( + segmentCondition: SegmentCondition, + context: EvaluationContext, + configSalt: String?, + segments: Array, + evaluateLogger: EvaluateLogger? + ): Boolean { + val segmentIndex: Int = segmentCondition.segmentIndex + var segment: Segment? = null + if (0 <= segmentIndex && segmentIndex < segments.size) { + segment = segments[segmentIndex] + } + evaluateLogger?.append(EvaluatorLogHelper.formatSegmentFlagCondition(segmentCondition, segment)) + + if (context.user == null) { + if (!context.isUserMissing) { + context.isUserMissing = true + this.logger.warning(3001, ConfigCatLogMessages.getUserObjectMissing(context.key)) + } + throw RolloutEvaluatorException(USER_OBJECT_IS_MISSING) + } + + require(segment != null) { "Segment reference is invalid." } + + val segmentName: String? = segment.name + require(!segmentName.isNullOrEmpty()) { "Segment name is missing." } + + evaluateLogger?.logSegmentEvaluationStart(segmentName) + var result: Boolean + @Suppress("SwallowedException") + try { + val segmentRulesResult = evaluateConditions( + segment.conditionAccessors, + null, + configSalt, + segmentName, + context, + segments, + evaluateLogger ) - return EvaluationResult(rule.value, rule.variationId, targetingRule = rule) + + val segmentComparator = segmentCondition.segmentComparator.toSegmentComparatorOrNull() + ?: throw IllegalArgumentException("Segment comparison operator is invalid.") + result = segmentRulesResult + if (SegmentComparator.IS_NOT_IN_SEGMENT == segmentComparator) { + result = !result + } + evaluateLogger?.logSegmentEvaluationResult(segmentCondition, segment, result, segmentRulesResult) + } catch (evaluatorException: RolloutEvaluatorException) { + evaluateLogger?.logSegmentEvaluationError(segmentCondition, segment, evaluatorException.message) + throw evaluatorException } - return null + + return result } - private fun processContains( - rule: RolloutRule, - userValue: String, - evaluatorLogger: EvaluatorLogger, - comparator: Comparator - ): EvaluationResult? { - val matchCondition = when (comparator) { - Comparator.CONTAINS -> userValue.contains(rule.comparisonValue) - Comparator.NOT_CONTAINS -> !userValue.contains(rule.comparisonValue) - else -> false - } - if (matchCondition) { - evaluatorLogger.logMatch( - rule.comparisonAttribute, - userValue, - comparator, - rule.comparisonValue, - rule.value + private fun evaluatePrerequisiteFlagCondition( + prerequisiteFlagCondition: PrerequisiteFlagCondition, + context: EvaluationContext, + evaluateLogger: EvaluateLogger? + ): Boolean { + evaluateLogger?.append(EvaluatorLogHelper.formatPrerequisiteFlagCondition(prerequisiteFlagCondition)) + val prerequisiteFlagKey: String? = prerequisiteFlagCondition.prerequisiteFlagKey + val prerequisiteFlagSetting = context.settings?.get(prerequisiteFlagKey) + require(!prerequisiteFlagKey.isNullOrEmpty() && prerequisiteFlagSetting != null) { + "Prerequisite flag key is missing or invalid." + } + + val settingType = prerequisiteFlagSetting.type.toSettingTypeOrNull() + require( + settingType == SettingType.JS_NUMBER && + ( + prerequisiteFlagCondition.value?.doubleValue != null || + prerequisiteFlagCondition.value?.integerValue != null + ) || + settingType == SettingType.BOOLEAN && prerequisiteFlagCondition.value?.booleanValue != null || + settingType == SettingType.STRING && prerequisiteFlagCondition.value?.stringValue != null || + settingType == SettingType.INT && prerequisiteFlagCondition.value?.integerValue != null || + settingType == SettingType.DOUBLE && prerequisiteFlagCondition.value?.doubleValue != null + ) { + "Type mismatch between comparison value '${prerequisiteFlagCondition.value}' and prerequisite flag " + + "'$prerequisiteFlagKey'." + } + + val visitedKeys: ArrayList = context.visitedKeys ?: ArrayList() + visitedKeys.add(context.key) + if (visitedKeys.contains(prerequisiteFlagKey)) { + val dependencyCycle: String = + EvaluatorLogHelper.formatCircularDependencyList(visitedKeys, prerequisiteFlagKey) + throw IllegalArgumentException( + "Circular dependency detected between the following depending flags: $dependencyCycle." ) - return EvaluationResult(rule.value, rule.variationId, targetingRule = rule) } - return null + evaluateLogger?.logPrerequisiteFlagEvaluationStart(prerequisiteFlagKey) + + val prerequisiteFlagContext = EvaluationContext( + prerequisiteFlagKey, + context.user, + visitedKeys, + context.settings + ) + + val evaluateResult = evaluateSetting( + prerequisiteFlagSetting, + evaluateLogger, + prerequisiteFlagContext + ) + visitedKeys.removeAt(visitedKeys.size - 1) + + Helpers.validateSettingValueType(evaluateResult.value, prerequisiteFlagSetting.type) + + val prerequisiteComparator = prerequisiteFlagCondition.prerequisiteComparator.toPrerequisiteComparatorOrNull() + ?: throw IllegalArgumentException("Prerequisite Flag comparison operator is invalid.") + + val conditionValue: SettingValue? = prerequisiteFlagCondition.value + var result = evaluateResult.value.equalsBasedOnSettingType(conditionValue, prerequisiteFlagSetting.type) + + if (PrerequisiteComparator.NOT_EQUALS == prerequisiteComparator) { + result = !result + } + evaluateLogger?.logPrerequisiteFlagEvaluationResult(prerequisiteFlagCondition, evaluateResult.value, result) + + return result } - private fun processSemverOneOf( - rule: RolloutRule, - userValue: String, - evaluatorLogger: EvaluatorLogger, - comparator: Comparator - ): EvaluationResult? { - try { - val userVersion = userValue.toVersion() - val split = rule.comparisonValue.split(",") - .map { it.trim() }.filter { it.isNotEmpty() } - var matched = false - for (value in split) { - matched = value.toVersion() == userVersion || matched - } - if ((matched && comparator == Comparator.ONE_OF_SEMVER) || - (!matched && comparator == Comparator.NOT_ONE_OF_SEMVER) - ) { - evaluatorLogger.logMatch( - rule.comparisonAttribute, - userValue, - comparator, - rule.comparisonValue, - rule.value - ) - return EvaluationResult(rule.value, rule.variationId, targetingRule = rule) + @Suppress("ThrowsCount", "ReturnCount", "CyclomaticComplexMethod", "LongMethod") + private fun evaluateUserCondition( + condition: UserCondition, + configSalt: String?, + context: EvaluationContext, + contextSalt: String, + evaluateLogger: EvaluateLogger? + ): Boolean { + evaluateLogger?.append(EvaluatorLogHelper.formatUserCondition(condition)) + if (context.user == null) { + if (!context.isUserMissing) { + context.isUserMissing = true + this.logger.warning(3001, ConfigCatLogMessages.getUserObjectMissing(context.key)) } - } catch (e: VersionFormatException) { - evaluatorLogger.logFormatError( - rule.comparisonAttribute, - userValue, - comparator, - rule.comparisonValue, - e + throw RolloutEvaluatorException(USER_OBJECT_IS_MISSING) + } + val comparisonAttribute = condition.comparisonAttribute + val userValue = context.user.attributeFor(comparisonAttribute) + val comparator = condition.comparator.toComparatorOrNull() + ?: throw IllegalArgumentException("Comparison operator is invalid.") + + if (userValue == null) { + logger.warning( + 3003, + ConfigCatLogMessages.getUserAttributeMissing(context.key, condition, comparisonAttribute) ) + throw RolloutEvaluatorException("cannot evaluate, the User.$comparisonAttribute attribute is missing") + } + + when (comparator) { + UserComparator.CONTAINS_ANY_OF, + UserComparator.NOT_CONTAINS_ANY_OF -> { + val negateContainsAnyOf = UserComparator.NOT_CONTAINS_ANY_OF == comparator + val userAttributeAsString = + getUserAttributeAsString(context.key, condition, comparisonAttribute, userValue) + return processContains(condition, userAttributeAsString, negateContainsAnyOf) + } + + UserComparator.ONE_OF_SEMVER, + UserComparator.NOT_ONE_OF_SEMVER -> { + val negateSemverIsOneOf: Boolean = UserComparator.NOT_ONE_OF_SEMVER == comparator + val userAttributeAsVersion = + getUserAttributeAsVersion(context.key, condition, comparisonAttribute, userValue) + return processSemVerOneOf(condition, userAttributeAsVersion, negateSemverIsOneOf) + } + + UserComparator.LT_SEMVER, + UserComparator.LTE_SEMVER, + UserComparator.GT_SEMVER, + UserComparator.GTE_SEMVER -> { + val userAttributeAsVersion = + getUserAttributeAsVersion(context.key, condition, comparisonAttribute, userValue) + return processSemVerCompare(condition, userAttributeAsVersion, comparator) + } + + UserComparator.EQ_NUM, + UserComparator.NOT_EQ_NUM, + UserComparator.LT_NUM, + UserComparator.LTE_NUM, + UserComparator.GT_NUM, + UserComparator.GTE_NUM -> { + val userAttributeAsDouble = + getUserAttributeAsDouble(context.key, condition, comparisonAttribute, userValue) + return processNumber(condition, userAttributeAsDouble, comparator) + } + + UserComparator.IS_ONE_OF, + UserComparator.IS_NOT_ONE_OF, + UserComparator.ONE_OF_SENS, + UserComparator.NOT_ONE_OF_SENS -> { + val negateIsOneOf = + UserComparator.NOT_ONE_OF_SENS == comparator || UserComparator.IS_NOT_ONE_OF == comparator + val sensitiveIsOneOf = + UserComparator.ONE_OF_SENS == comparator || UserComparator.NOT_ONE_OF_SENS == comparator + + val userAttributeAsString = + getUserAttributeAsString(context.key, condition, comparisonAttribute, userValue) + return processSensitiveOneOf( + condition, + userAttributeAsString, + configSalt, + contextSalt, + negateIsOneOf, + sensitiveIsOneOf + ) + } + + UserComparator.DATE_BEFORE, + UserComparator.DATE_AFTER -> { + val userAttributeForDate = getUserAttributeForDate(condition, context, comparisonAttribute, userValue) + return processDateCompare(condition, userAttributeForDate, comparator) + } + + UserComparator.TEXT_EQUALS, + UserComparator.TEXT_NOT_EQUALS, + UserComparator.HASHED_EQUALS, + UserComparator.HASHED_NOT_EQUALS -> { + val negateEquals = + UserComparator.HASHED_NOT_EQUALS == comparator || UserComparator.TEXT_NOT_EQUALS == comparator + val hashedEquals = + UserComparator.HASHED_EQUALS == comparator || UserComparator.HASHED_NOT_EQUALS == comparator + + val userAttributeAsString = + getUserAttributeAsString(context.key, condition, comparisonAttribute, userValue) + return processHashedEqualsCompare( + condition, + userAttributeAsString, + configSalt, + contextSalt, + negateEquals, + hashedEquals + ) + } + + UserComparator.HASHED_STARTS_WITH, + UserComparator.HASHED_NOT_STARTS_WITH, + UserComparator.HASHED_ENDS_WITH, + UserComparator.HASHED_NOT_ENDS_WITH -> { + val userAttributeAsString = + getUserAttributeAsString(context.key, condition, comparisonAttribute, userValue) + return processHashedStartEndsWithCompare( + condition, + userAttributeAsString, + ensureConfigSalt(configSalt), + contextSalt, + comparator + ) + } + + UserComparator.TEXT_STARTS_WITH, + UserComparator.TEXT_NOT_STARTS_WITH -> { + val negateTextStartWith = UserComparator.TEXT_NOT_STARTS_WITH == comparator + val userAttributeAsString = + getUserAttributeAsString(context.key, condition, comparisonAttribute, userValue) + return processTextStartWithCompare(condition, userAttributeAsString, negateTextStartWith) + } + + UserComparator.TEXT_ENDS_WITH, + UserComparator.TEXT_NOT_ENDS_WITH -> { + val negateTextEndsWith = UserComparator.TEXT_NOT_ENDS_WITH == comparator + val userAttributeAsString = + getUserAttributeAsString(context.key, condition, comparisonAttribute, userValue) + return processTextEndWithCompare(condition, userAttributeAsString, negateTextEndsWith) + } + + UserComparator.TEXT_ARRAY_CONTAINS, + UserComparator.TEXT_ARRAY_NOT_CONTAINS, + UserComparator.HASHED_ARRAY_CONTAINS, + UserComparator.HASHED_ARRAY_NOT_CONTAINS -> { + val negateArrayContains = + UserComparator.HASHED_ARRAY_NOT_CONTAINS == comparator || + UserComparator.TEXT_ARRAY_NOT_CONTAINS == comparator + val hashedArrayContains = + UserComparator.HASHED_ARRAY_CONTAINS == comparator || + UserComparator.HASHED_ARRAY_NOT_CONTAINS == comparator + val userArrayValue = getUserAttributeAsStringArray(condition, context, comparisonAttribute, userValue) + return processHashedArrayContainsCompare( + condition, + userArrayValue, + configSalt, + contextSalt, + negateArrayContains, + hashedArrayContains + ) + } } - return null } - private fun processSemverCompare( - rule: RolloutRule, + private fun processContains( + condition: UserCondition, userValue: String, - evaluatorLogger: EvaluatorLogger, - comparator: Comparator - ): EvaluationResult? { - try { - val userVersion = userValue.toVersion() - val comparisonVersion = rule.comparisonValue.trim().toVersion() - val matchCondition = when (comparator) { - Comparator.LT_SEMVER -> userVersion < comparisonVersion - Comparator.LTE_SEMVER -> userVersion <= comparisonVersion - Comparator.GT_SEMVER -> userVersion > comparisonVersion - Comparator.GTE_SEMVER -> userVersion >= comparisonVersion - else -> false - } - if (matchCondition) { - evaluatorLogger.logMatch( - rule.comparisonAttribute, - userValue, - comparator, - rule.comparisonValue, - rule.value - ) - return EvaluationResult(rule.value, rule.variationId, targetingRule = rule) + negate: Boolean + ): Boolean { + val comparisonValues = ensureComparisonValue(condition.stringArrayValue) + for (containsValue in comparisonValues) { + if (userValue.contains(ensureComparisonValue(containsValue))) { + return !negate } - } catch (e: VersionFormatException) { - evaluatorLogger.logFormatError( - rule.comparisonAttribute, - userValue, - comparator, - rule.comparisonValue, - e - ) } - return null + return negate + } + + private fun processSemVerOneOf( + condition: UserCondition, + userVersion: Version, + negate: Boolean + ): Boolean { + val comparisonValues = ensureComparisonValue(condition.stringArrayValue) + var matched = false + for (semVer in comparisonValues) { + // Previous versions of the evaluation algorithm ignore empty comparison values. + // We keep this behavior for backward compatibility. + if (ensureComparisonValue(semVer).isEmpty()) { + continue + } + val comparisonSemVer = semVer.trim().toVersionOrNull() + matched = if (comparisonSemVer == null) { + false + } else { + comparisonSemVer == userVersion || matched + } + } + + return negate != matched + } + + private fun processSemVerCompare( + condition: UserCondition, + userVersion: Version, + userComparator: UserComparator + ): Boolean { + val comparisonVersion = ensureComparisonValue(condition.stringValue).trim().toVersionOrNull() ?: return false + return when (userComparator) { + UserComparator.LT_SEMVER -> userVersion < comparisonVersion + UserComparator.LTE_SEMVER -> userVersion <= comparisonVersion + UserComparator.GT_SEMVER -> userVersion > comparisonVersion + UserComparator.GTE_SEMVER -> userVersion >= comparisonVersion + else -> error("Invalid comparator $userComparator.") + } } private fun processNumber( - rule: RolloutRule, + condition: UserCondition, + userNumber: Double, + userComparator: UserComparator + ): Boolean { + val comparisonNumber = ensureComparisonValue(condition.doubleValue) + return when (userComparator) { + UserComparator.EQ_NUM -> userNumber == comparisonNumber + UserComparator.NOT_EQ_NUM -> userNumber != comparisonNumber + UserComparator.LT_NUM -> userNumber < comparisonNumber + UserComparator.LTE_NUM -> userNumber <= comparisonNumber + UserComparator.GT_NUM -> userNumber > comparisonNumber + UserComparator.GTE_NUM -> userNumber >= comparisonNumber + else -> error("Invalid comparator $userComparator.") + } + } + + private fun processSensitiveOneOf( + condition: UserCondition, userValue: String, - evaluatorLogger: EvaluatorLogger, - comparator: Comparator - ): EvaluationResult? { - try { - val userNumber = userValue.replace(",", ".").toDouble() - val comparisonNumber = rule.comparisonValue.trim().replace(",", ".").toDouble() - val matchCondition = when (comparator) { - Comparator.EQ_NUM -> userNumber == comparisonNumber - Comparator.NOT_EQ_NUM -> userNumber != comparisonNumber - Comparator.LT_NUM -> userNumber < comparisonNumber - Comparator.LTE_NUM -> userNumber <= comparisonNumber - Comparator.GT_NUM -> userNumber > comparisonNumber - Comparator.GTE_NUM -> userNumber >= comparisonNumber - else -> false - } - if (matchCondition) { - evaluatorLogger.logMatch( - rule.comparisonAttribute, - userValue, - comparator, - rule.comparisonValue, - rule.value - ) - return EvaluationResult(rule.value, rule.variationId, targetingRule = rule) + configSalt: String?, + contextSalt: String, + negateIsOneOf: Boolean, + sensitiveIsOneOf: Boolean + ): Boolean { + val comparisonValues = ensureComparisonValue(condition.stringArrayValue) + val userIsOneOfValue: String = if (sensitiveIsOneOf) { + getSaltedUserValue(userValue, ensureConfigSalt(configSalt), contextSalt) + } else { + userValue + } + + for (inValuesElement in comparisonValues) { + if (ensureComparisonValue(inValuesElement) == userIsOneOfValue) { + return !negateIsOneOf } - } catch (e: NumberFormatException) { - evaluatorLogger.logFormatError( - rule.comparisonAttribute, - userValue, - comparator, - rule.comparisonValue, - e - ) } - return null + return negateIsOneOf } - private fun processSensitiveOneOf( - rule: RolloutRule, + private fun processDateCompare( + condition: UserCondition, + userDateDouble: Double, + userComparator: UserComparator + ): Boolean { + val comparisonDateDouble = + ensureComparisonValue(condition.doubleValue) + return when (userComparator) { + UserComparator.DATE_BEFORE -> userDateDouble < comparisonDateDouble + UserComparator.DATE_AFTER -> userDateDouble > comparisonDateDouble + else -> error("Invalid comparator $userComparator.") + } + } + + private fun processHashedEqualsCompare( + condition: UserCondition, userValue: String, - evaluatorLogger: EvaluatorLogger, - comparator: Comparator - ): EvaluationResult? { - val split = rule.comparisonValue.split(",").map { it.trim() }.filter { it.isNotEmpty() } - val userValueHash = userValue.encodeToByteArray().sha1().hex - val matchCondition = when (comparator) { - Comparator.ONE_OF_SENS -> split.contains(userValueHash) - Comparator.NOT_ONE_OF_SENS -> !split.contains(userValueHash) - else -> false - } - if (matchCondition) { - evaluatorLogger.logMatch( - rule.comparisonAttribute, - userValue, - comparator, - rule.comparisonValue, - rule.value - ) - return EvaluationResult(rule.value, rule.variationId, targetingRule = rule) + configSalt: String?, + contextSalt: String, + negateEquals: Boolean, + hashedEquals: Boolean + ): Boolean { + val comparisonValue = ensureComparisonValue(condition.stringValue) + val valueEquals = if (hashedEquals) { + getSaltedUserValue(userValue, ensureConfigSalt(configSalt), contextSalt) + } else { + userValue } - return null + + return negateEquals != (valueEquals == comparisonValue) } - private fun processPercentageRules( - setting: Setting, - user: ConfigCatUser, - key: String, - evaluatorLogger: EvaluatorLogger + private fun processHashedStartEndsWithCompare( + condition: UserCondition, + userValue: String, + configSalt: String, + contextSalt: String, + userComparator: UserComparator + ): Boolean { + val withValuesSplit = ensureComparisonValue(condition.stringArrayValue) + val userValueUTF8 = userValue.encodeToByteArray() + var matchCondition = false + @Suppress("LoopWithTooManyJumpStatements") + for (comparisonValueHashedStartsEnds in withValuesSplit) { + val comparedTextLength = ensureComparisonValue(comparisonValueHashedStartsEnds).substringBefore("_") + require(comparedTextLength != comparisonValueHashedStartsEnds) { + "Comparison value is missing or invalid." + } + val comparedTextLengthInt: Int + try { + comparedTextLengthInt = comparedTextLength.trim().toInt() + } catch (e: NumberFormatException) { + throw IllegalArgumentException("Comparison value is missing or invalid.") + } + if (userValueUTF8.size < comparedTextLengthInt) { + continue + } + val comparisonHashValue = comparisonValueHashedStartsEnds.substringAfter("_") + require(comparisonHashValue.isNotEmpty()) { "Comparison value is missing or invalid." } + val userValueSlice = + if (userComparator == UserComparator.HASHED_STARTS_WITH || + userComparator == UserComparator.HASHED_NOT_STARTS_WITH + ) { + userValueUTF8.copyOfRange(0, comparedTextLengthInt) + } else { + // Comparator.HASHED_ENDS_WITH, Comparator.HASHED_NOT_ENDS_WITH + userValueUTF8.copyOfRange(userValueUTF8.size - comparedTextLengthInt, userValueUTF8.size) + } + val userValueHashed = getSaltedUserValueSlice(userValueSlice, configSalt, contextSalt) + if (userValueHashed == comparisonHashValue) { + matchCondition = true + break + } + } + if (userComparator == UserComparator.HASHED_NOT_STARTS_WITH || + userComparator == UserComparator.HASHED_NOT_ENDS_WITH + ) { + // negate the match in case of NOT ANY OF + matchCondition = !matchCondition + } + return matchCondition + } + + private fun processTextStartWithCompare( + condition: UserCondition, + userValue: String, + negateTextStartWith: Boolean + ): Boolean { + val comparisonValues = ensureComparisonValue(condition.stringArrayValue) + for (textValue in comparisonValues) { + if (userValue.startsWith(ensureComparisonValue(textValue))) { + return !negateTextStartWith + } + } + return negateTextStartWith + } + + private fun processTextEndWithCompare( + condition: UserCondition, + userValue: String, + negateTextEndsWith: Boolean + ): Boolean { + val comparisonValues = ensureComparisonValue(condition.stringArrayValue) + for (textValue in comparisonValues) { + if (userValue.endsWith(ensureComparisonValue(textValue))) { + return !negateTextEndsWith + } + } + return negateTextEndsWith + } + + private fun processHashedArrayContainsCompare( + condition: UserCondition, + userContainsArray: Array, + configSalt: String?, + contextSalt: String, + negateArrayContains: Boolean, + hashedArrayContains: Boolean + ): Boolean { + val comparisonValues = ensureComparisonValue(condition.stringArrayValue) + if (userContainsArray.isEmpty()) { + return negateArrayContains + } + for (userContainsValue in userContainsArray) { + val userContainsValueConverted = if (hashedArrayContains) { + getSaltedUserValue( + userContainsValue, + ensureConfigSalt(configSalt), + contextSalt + ) + } else { + userContainsValue + } + for (inValuesElement in comparisonValues) { + if (ensureComparisonValue(inValuesElement) == userContainsValueConverted) { + return !negateArrayContains + } + } + } + return negateArrayContains + } + + private fun getSaltedUserValue(userValue: String, configSalt: String, contextSalt: String): String { + val value = userValue + configSalt + contextSalt + return value.encodeToByteArray().sha256().hex + } + + private fun getSaltedUserValueSlice(userValue: ByteArray, configSalt: String, contextSalt: String): String { + val configSaltByteArray = configSalt.encodeToByteArray() + val contextSaltByteArray = contextSalt.encodeToByteArray() + val concatByteArray = userValue + configSaltByteArray + contextSaltByteArray + return concatByteArray.sha256().hex + } + + private fun evaluatePercentageOptions( + percentageOptions: Array, + percentageOptionAttribute: String?, + context: EvaluationContext, + parentTargetingRule: TargetingRule?, + evaluateLogger: EvaluateLogger? ): EvaluationResult? { - if (setting.percentageItems.isEmpty()) { + if (context.user == null) { + evaluateLogger?.logPercentageOptionUserMissing() + if (!context.isUserMissing) { + context.isUserMissing = true + this.logger.warning(3001, ConfigCatLogMessages.getUserObjectMissing(context.key)) + } return null } - val hashCandidate = "$key${user.identifier}" + val percentageOptionAttributeValue: String? + var percentageOptionAttributeName = percentageOptionAttribute + if (percentageOptionAttributeName == null) { + percentageOptionAttributeName = "Identifier" + percentageOptionAttributeValue = context.user.identifier + } else { + percentageOptionAttributeValue = + userAttributeToString(context.user.attributeFor(percentageOptionAttributeName)) + if (percentageOptionAttributeValue == null) { + evaluateLogger?.logPercentageOptionUserAttributeMissing(percentageOptionAttributeName) + if (!context.isUserAttributeMissing) { + context.isUserAttributeMissing = true + this.logger.warning( + 3003, + ConfigCatLogMessages.getUserAttributeMissing(context.key, percentageOptionAttributeName) + ) + } + return null + } + } + evaluateLogger?.logPercentageOptionEvaluation(percentageOptionAttributeName) + + val hashCandidate = "${context.key}$percentageOptionAttributeValue" val hash = hashCandidate.encodeToByteArray().sha1().hex.substring(0, 7) val numberRepresentation = hash.toInt(radix = 16) val scale = numberRepresentation % 100 + evaluateLogger?.logPercentageOptionEvaluationHash(percentageOptionAttributeName, scale) var bucket = 0.0 - for (rule in setting.percentageItems) { + for (i in percentageOptions.indices) { + val rule = percentageOptions[i] bucket += rule.percentage if (scale < bucket) { - evaluatorLogger.logPercentageEvaluationReturnValue(rule.value) - return EvaluationResult(rule.value, rule.variationId, percentageRule = rule) + evaluateLogger?.logPercentageEvaluationReturnValue(scale, i, rule.percentage, rule.value) + return EvaluationResult(rule.value, rule.variationId, parentTargetingRule, rule) } } - return null + + throw IllegalArgumentException("Sum of percentage option percentages is less than 100.") } - enum class Comparator(val value: String) { - ONE_OF("IS ONE OF"), - NOT_ONE_OF("IS NOT ONE OF"), - CONTAINS("CONTAINS"), - NOT_CONTAINS("DOES NOT CONTAIN"), - ONE_OF_SEMVER("IS ONE OF (SemVer)"), - NOT_ONE_OF_SEMVER("IS NOT ONE OF (SemVer)"), - LT_SEMVER("< (SemVer)"), - LTE_SEMVER("<= (SemVer)"), - GT_SEMVER("> (SemVer)"), - GTE_SEMVER(">= (SemVer)"), - EQ_NUM("= (Number)"), - NOT_EQ_NUM("<> (Number)"), - LT_NUM("< (Number)"), - LTE_NUM("<= (Number)"), - GT_NUM("> (Number)"), - GTE_NUM(">= (Number)"), - ONE_OF_SENS("IS ONE OF (Sensitive)"), - NOT_ONE_OF_SENS("IS NOT ONE OF (Sensitive)") - } - - private fun Int.toComparatorOrNull(): Comparator? = Comparator.values().firstOrNull { it.ordinal == this } -} + private fun getUserAttributeAsStringArray( + userCondition: UserCondition, + context: EvaluationContext, + comparisonAttribute: String, + userAttribute: Any + ): Array { + @Suppress("SwallowedException") + try { + if (userAttribute is Array<*> && userAttribute.all { it is String }) { + return userAttribute as Array + } + if ((userAttribute is List<*>) && userAttribute.all { it is String }) { + val stringList: List = userAttribute as List + return stringList.toTypedArray() + } + if (userAttribute is String) { + return Constants.json.decodeFromString(userAttribute) + } + } catch (exception: Exception) { + // if exception or no return yet, then throw RolloutEvaluatorException + } + val reason = "'$userAttribute' is not a valid JSON string array" + logger.warning( + 3004, + ConfigCatLogMessages.getUserAttributeInvalid( + context.key, + userCondition, + reason, + comparisonAttribute + ) + ) + throw RolloutEvaluatorException( + "cannot evaluate, the User.$comparisonAttribute attribute is " + + "invalid ($reason)" + ) + } + + private fun getUserAttributeAsVersion( + key: String, + userCondition: UserCondition, + comparisonAttribute: String, + userValue: Any + ): Version { + @Suppress("SwallowedException") + try { + if (userValue is String) { + return userValue.trim().toVersion() + } + } catch (e: VersionFormatException) { + // Version parse failed continue with the RolloutEvaluatorException + } + val reason = "'$userValue' is not a valid semantic version" + logger.warning( + 3004, + ConfigCatLogMessages.getUserAttributeInvalid( + key, + userCondition, + reason, + comparisonAttribute + ) + ) + throw RolloutEvaluatorException( + "cannot evaluate, the User.$comparisonAttribute attribute is " + + "invalid ($reason)" + ) + } -internal class EvaluatorLogger constructor( - key: String -) { - private val entries = StringBuilder() + private fun getUserAttributeAsDouble( + key: String, + userCondition: UserCondition, + comparisonAttribute: String, + userValue: Any + ): Double { + try { + return if (userValue is Double) { + userValue + } else { + userAttributeToDouble(userValue) + } + } catch (e: NumberFormatException) { + val reason = "'$userValue' is not a valid decimal number" + logger.warning( + 3004, + ConfigCatLogMessages.getUserAttributeInvalid( + key, + userCondition, + reason, + comparisonAttribute + ) + ) + throw RolloutEvaluatorException( + "cannot evaluate, the User.$comparisonAttribute attribute is " + + "invalid ($reason)" + ) + } + } - init { - entries.appendLine("Evaluating '$key'") + private fun getUserAttributeForDate( + userCondition: UserCondition, + context: EvaluationContext, + comparisonAttribute: String, + userValue: Any + ): Double { + try { + if (userValue is DateTime) { + return userValue.unixMillisDouble / 1000 + } + if (userValue is DateTimeTz) { + return userValue.local.unixMillisDouble / 1000 + } + return userAttributeToDouble(userValue) + } catch (e: NumberFormatException) { + val reason = + "'$userValue' is not a valid Unix timestamp (number of seconds elapsed since Unix epoch)" + logger.warning( + 3004, + ConfigCatLogMessages.getUserAttributeInvalid( + context.key, + userCondition, + reason, + comparisonAttribute + ) + ) + throw RolloutEvaluatorException( + "cannot evaluate, the User.$comparisonAttribute attribute is " + + "invalid ($reason)" + ) + } } - fun logReturnValue(value: Any) { - entries.appendLine("Returning $value") + private fun getUserAttributeAsString( + key: String, + userCondition: UserCondition, + userAttributeName: String, + userValue: Any + ): String { + if (userValue is String) { + return userValue + } + val userAttributeToString = userAttributeToString(userValue) ?: "" + logger.warning( + 3005, + ConfigCatLogMessages.getUserObjectAttributeIsAutoConverted( + key, + userCondition, + userAttributeName, + userAttributeToString + ) + ) + + return userAttributeToString } - fun logPercentageEvaluationReturnValue(value: Any) { - entries.appendLine("Evaluating % options. Returning $value.") + private fun userAttributeToString(userValue: Any?): String? { + if (userValue == null) { + return null + } + if (userValue is String) { + return userValue + } + if (userValue is Array<*> && userValue.all { it is String }) { + return Constants.json.encodeToString(userValue as Array) + } + if (userValue is List<*> && userValue.all { it is String }) { + return Constants.json.encodeToString(userValue as List) + } + if (userValue is Float) { + return doubleToString(userValue.toDouble()) + } + if (userValue is Double) { + return doubleToString(userValue) + } + + if (userValue is DateTime) { + return doubleToString((userValue.unixMillisDouble / 1000)) + } + if (userValue is DateTimeTz) { + return doubleToString((userValue.local.unixMillisDouble / 1000)) + } + return userValue.toString() } - fun logUserObject(user: ConfigCatUser) { - entries.appendLine("User object: $user") + private fun userAttributeToDouble(userValue: Any): Double { + if (userValue is Double) { + return userValue + } + if (userValue is Float) { + return userValue.toDouble() + } + if (userValue is Int) { + return userValue.toDouble() + } + if (userValue is Long) { + return userValue.toDouble() + } + if (userValue is Byte) { + return userValue.toDouble() + } + if (userValue is Short) { + return userValue.toDouble() + } + if (userValue is String) { + return userValue.trim().replace(",", ".").toDouble() + } + + throw NumberFormatException() } - fun logMatch( - attribute: String, - userValue: String, - comparator: Evaluator.Comparator, - comparisonValue: String, - value: Any? - ) { - entries.appendLine( - "Evaluating rule: [$attribute:$userValue] " + - "[${comparator.value}] [$comparisonValue] => match, returning: $value" - ) + private fun ensureConfigSalt(configSalt: String?): String { + if (configSalt != null) { + return configSalt + } + throw IllegalArgumentException("Config JSON salt is missing.") } - fun logNoMatch( - attribute: String, - userValue: String, - comparator: Evaluator.Comparator, - comparisonValue: String - ) { - entries.appendLine( - "Evaluating rule: " + - "[$attribute:$userValue] [${comparator.value}] [$comparisonValue] => no match" - ) + private inline fun ensureComparisonValue(value: T?): T { + if (value != null) { + return value + } + throw IllegalArgumentException("Comparison value is missing or invalid.") } - fun logFormatError( - attribute: String, - userValue: String, - comparator: Evaluator.Comparator, - comparisonValue: String, - error: Throwable - ) { - entries.appendLine( - "Evaluating rule: [$attribute:$userValue] [${comparator.value}] " + - "[$comparisonValue] => SKIP rule. Validation error: ${error.message}" - ) + /** + * Describes the Rollout Evaluator User Condition Comparators. + */ + enum class UserComparator(val id: Int, val value: String) { + IS_ONE_OF(0, "IS ONE OF"), + IS_NOT_ONE_OF(1, "IS NOT ONE OF"), + CONTAINS_ANY_OF(2, "CONTAINS ANY OF"), + NOT_CONTAINS_ANY_OF(3, "NOT CONTAINS ANY OF"), + ONE_OF_SEMVER(4, "IS ONE OF"), + NOT_ONE_OF_SEMVER(5, "IS NOT ONE OF"), + LT_SEMVER(6, "<"), + LTE_SEMVER(7, "<="), + GT_SEMVER(8, ">"), + GTE_SEMVER(9, ">="), + EQ_NUM(10, "="), + NOT_EQ_NUM(11, "!="), + LT_NUM(12, "<"), + LTE_NUM(13, "<="), + GT_NUM(14, ">"), + GTE_NUM(15, ">="), + ONE_OF_SENS(16, "IS ONE OF"), + NOT_ONE_OF_SENS(17, "IS NOT ONE OF"), + DATE_BEFORE(18, "BEFORE"), + DATE_AFTER(19, "AFTER"), + HASHED_EQUALS(20, "EQUALS"), + HASHED_NOT_EQUALS(21, "NOT EQUALS"), + HASHED_STARTS_WITH(22, "STARTS WITH ANY OF"), + HASHED_NOT_STARTS_WITH(23, "NOT STARTS WITH ANY OF"), + HASHED_ENDS_WITH(24, "ENDS WITH ANY OF"), + HASHED_NOT_ENDS_WITH(25, "NOT ENDS WITH ANY OF"), + HASHED_ARRAY_CONTAINS(26, "ARRAY CONTAINS ANY OF"), + HASHED_ARRAY_NOT_CONTAINS(27, "ARRAY NOT CONTAINS ANY OF"), + TEXT_EQUALS(28, "EQUALS"), + TEXT_NOT_EQUALS(29, "NOT EQUALS"), + TEXT_STARTS_WITH(30, "STARTS WITH ANY OF"), + TEXT_NOT_STARTS_WITH(31, "NOT STARTS WITH ANY OF"), + TEXT_ENDS_WITH(32, "ENDS WITH ANY OF"), + TEXT_NOT_ENDS_WITH(33, "NOT ENDS WITH ANY OF"), + TEXT_ARRAY_CONTAINS(34, "ARRAY CONTAINS ANY OF"), + TEXT_ARRAY_NOT_CONTAINS(35, "ARRAY NOT CONTAINS ANY OF"); } - fun logComparatorError( - attribute: String, - userValue: String, - comparator: Int, - comparisonValue: String - ) { - entries.appendLine( - "Evaluating rule: [$attribute:$userValue] [$comparisonValue] => SKIP rule. Invalid comparator: $comparator" - ) + /** + * Describes the Prerequisite Comparators. + */ + enum class PrerequisiteComparator(val id: Int, val value: String) { + EQUALS(0, "EQUALS"), + NOT_EQUALS(1, "NOT EQUALS") + } + + /** + * Describes the Segment Comparators. + */ + enum class SegmentComparator(val id: Int, val value: String) { + IS_IN_SEGMENT(0, "IS IN SEGMENT"), + IS_NOT_IN_SEGMENT(1, "IS NOT IN SEGMENT") } +} + +/** + * Convert [Double] to [String] based on the following format rules. + * + * To get similar result between different SDKs the Double value format is modified. + * Between 1e-6 and 1e21 we don't use scientific-notation. Over these limits scientific-notation used but the + * ExponentSeparator replaced with "e" and "e+". "." used as decimal separator in all cases. + * + * For [Double.NaN], [Double.POSITIVE_INFINITY] and [Double.NEGATIVE_INFINITY] simple String representation used. + */ +internal expect fun doubleToString(doubleToString: Double): String + +/** + * Format [Double] value for logging. + * + **/ +internal expect fun formatDoubleForLog(doubleToFormat: Double): String + +internal fun commonDoubleToString(doubleToString: Double): String { + if (doubleToString.isNaN() || doubleToString.isInfinite()) { + return doubleToString.toString() + } + // Scientific Notation use cannot be turned on or off in native and no formatter can be used properly. + // As best effort we replace the "," and the "E" if presented. + val stringFormatScientificNotation = doubleToString.toString().replace(",", ".") + return if (doubleToString.absoluteValue > 1) { + stringFormatScientificNotation.replace("E", "e+") + } else { + stringFormatScientificNotation.replace("E-", "e-") + } +} - fun print(): String { - return entries.toString() +internal fun commonFormatDoubleForLog(doubleToFormat: Double): String { + val comparisonValueString = doubleToFormat.toString().replace(',', '.') + return if (comparisonValueString.contains('.') || comparisonValueString.contains(',')) { + comparisonValueString.trimEnd { it == '0' }.trimEnd { it == '.' } + } else { + comparisonValueString } } + +internal class RolloutEvaluatorException(message: String?) : Exception(message) diff --git a/src/commonMain/kotlin/com/configcat/Hooks.kt b/src/commonMain/kotlin/com/configcat/Hooks.kt index 9b8e56f3..cea53962 100644 --- a/src/commonMain/kotlin/com/configcat/Hooks.kt +++ b/src/commonMain/kotlin/com/configcat/Hooks.kt @@ -1,5 +1,6 @@ package com.configcat +import com.configcat.model.Setting import kotlinx.atomicfu.locks.ReentrantLock import kotlinx.atomicfu.locks.reentrantLock import kotlinx.atomicfu.locks.withLock @@ -64,10 +65,10 @@ public class Hooks { } } - internal fun invokeOnConfigChanged(settings: Map) { + internal fun invokeOnConfigChanged(settings: Map?) { lock.withLock { for (method in onConfigChanged) { - method(settings) + method(settings ?: mapOf()) } } } diff --git a/src/commonMain/kotlin/com/configcat/Utils.kt b/src/commonMain/kotlin/com/configcat/Utils.kt deleted file mode 100644 index 97dc2643..00000000 --- a/src/commonMain/kotlin/com/configcat/Utils.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.configcat - -import com.soywiz.klock.DateTime -import kotlinx.serialization.json.Json -import kotlinx.serialization.modules.SerializersModule - -internal interface Closeable { - fun close() -} - -internal object Constants { - const val version: String = "2.1.0" - const val configFileName: String = "config_v5.json" - const val serializationFormatVersion: String = "v2" - const val globalCdnUrl = "https://cdn-global.configcat.com" - const val euCdnUrl = "https://cdn-eu.configcat.com" - - val distantPast = DateTime.fromUnix(0) - val distantFuture = DateTime.now().add(10_000, 0.0) - val json = Json { - ignoreUnknownKeys = true - serializersModule = SerializersModule { - contextual(Any::class, FlagValueSerializer) - } - } -} diff --git a/src/commonMain/kotlin/com/configcat/fetch/ConfigFetcher.kt b/src/commonMain/kotlin/com/configcat/fetch/ConfigFetcher.kt index e37f7a0a..0702d738 100644 --- a/src/commonMain/kotlin/com/configcat/fetch/ConfigFetcher.kt +++ b/src/commonMain/kotlin/com/configcat/fetch/ConfigFetcher.kt @@ -1,18 +1,18 @@ package com.configcat.fetch import com.configcat.* -import com.configcat.Closeable -import com.configcat.Constants import com.configcat.log.ConfigCatLogMessages import com.configcat.log.InternalLogger +import com.configcat.model.Config +import com.configcat.model.Entry import com.soywiz.klock.DateTime import io.ktor.client.* import io.ktor.client.plugins.* import io.ktor.client.request.* import io.ktor.client.statement.* import io.ktor.http.* -import kotlinx.atomicfu.* -import kotlinx.serialization.decodeFromString +import kotlinx.atomicfu.atomic +import kotlinx.atomicfu.update internal class ConfigFetcher constructor( private val options: ConfigCatOptions, @@ -20,7 +20,7 @@ internal class ConfigFetcher constructor( ) : Closeable { private val httpClient = createClient() private val closed = atomic(false) - private val isUrlCustom = !options.baseUrl.isNullOrEmpty() + private val isUrlCustom = options.isBaseURLCustom() private val baseUrl = atomic( options.baseUrl?.let { it.ifEmpty { null } } ?: if (options.dataGovernance == DataGovernance.GLOBAL) { @@ -71,20 +71,23 @@ internal class ConfigFetcher constructor( private suspend fun fetchHTTP(baseUrl: String, eTag: String): FetchResponse { val url = "$baseUrl/configuration-files/${options.sdkKey}/${Constants.configFileName}" try { + val httpRequestBuilder = + httpRequestBuilder("ConfigCat-Kotlin/${options.pollingMode.identifier}-${Constants.version}", eTag) val response = httpClient.get(url) { - headers { - append( - "X-ConfigCat-UserAgent", - "ConfigCat-Kotlin/${options.pollingMode.identifier}-${Constants.version}" - ) - if (eTag.isNotEmpty()) append(HttpHeaders.IfNoneMatch, eTag) + httpRequestBuilder.headers.entries().forEach { + headers.appendAll(it.key, it.value) + } + url { + httpRequestBuilder.url.parameters.entries().forEach { + parameters.appendAll(it.key, it.value) + } } } if (response.status.value in 200..299) { logger.debug("Fetch was successful: new config fetched.") val body = response.bodyAsText() val newETag = response.etag() - val (config, err) = parseConfigJson(body) + val (config, err) = deserializeConfig(body) if (err != null) { return FetchResponse.failure(err, true) } @@ -134,12 +137,26 @@ internal class ConfigFetcher constructor( } } - private fun parseConfigJson(jsonString: String): Pair { + private fun deserializeConfig(jsonString: String): Pair { return try { - Pair(Constants.json.decodeFromString(jsonString), null) + Pair(Helpers.parseConfigJson(jsonString), null) } catch (e: Exception) { logger.error(1105, ConfigCatLogMessages.FETCH_RECEIVED_200_WITH_INVALID_BODY_ERROR, e) Pair(Config.empty, e.message) } } } + +internal expect fun httpRequestBuilder(configCatUserAgent: String, eTag: String): HttpRequestBuilder + +internal fun commonHttpRequestBuilder(configCatUserAgent: String, eTag: String): HttpRequestBuilder { + val httpRequestBuilder = HttpRequestBuilder() + httpRequestBuilder.headers { + append( + "X-ConfigCat-UserAgent", + configCatUserAgent + ) + if (eTag.isNotEmpty()) append(HttpHeaders.IfNoneMatch, eTag) + } + return httpRequestBuilder +} diff --git a/src/commonMain/kotlin/com/configcat/fetch/FetchResponse.kt b/src/commonMain/kotlin/com/configcat/fetch/FetchResponse.kt index 59d3628a..9d9461df 100644 --- a/src/commonMain/kotlin/com/configcat/fetch/FetchResponse.kt +++ b/src/commonMain/kotlin/com/configcat/fetch/FetchResponse.kt @@ -1,6 +1,6 @@ package com.configcat.fetch -import com.configcat.Entry +import com.configcat.model.Entry internal enum class FetchStatus { FETCHED, diff --git a/src/commonMain/kotlin/com/configcat/log/ConfigCatLogMessages.kt b/src/commonMain/kotlin/com/configcat/log/ConfigCatLogMessages.kt index 3cee0367..800af1ed 100644 --- a/src/commonMain/kotlin/com/configcat/log/ConfigCatLogMessages.kt +++ b/src/commonMain/kotlin/com/configcat/log/ConfigCatLogMessages.kt @@ -1,5 +1,8 @@ package com.configcat.log +import com.configcat.EvaluatorLogHelper +import com.configcat.model.UserCondition + @Suppress("TooManyFunctions") internal object ConfigCatLogMessages { /** @@ -42,7 +45,10 @@ internal object ConfigCatLogMessages { /** * Log message for Fetch Failed Due To Unexpected error. The log eventId is 1103. */ - const val FETCH_FAILED_DUE_TO_UNEXPECTED_ERROR = "Unexpected error occurred while trying to fetch config JSON." + const val FETCH_FAILED_DUE_TO_UNEXPECTED_ERROR = + "Unexpected error occurred while trying to fetch config JSON. It is most likely due to a local network " + + "issue. Please make sure your application can reach the ConfigCat CDN servers (or your proxy server) " + + "over HTTP." /** * Log message for Fetch Failed Due To Invalid Sdk Key error. The log eventId is 1100. @@ -97,12 +103,42 @@ internal object ConfigCatLogMessages { ): String { return "Failed to evaluate setting '$key' (the key was not found in config JSON). " + "Returning the `$defaultParamName` parameter that you specified in your " + - "application: '$defaultParamValue'" + ". Available keys: [" + availableKeysSet.joinToString( + "application: '$defaultParamValue'. Available keys: [" + availableKeysSet.joinToString( ", ", transform = { availableKey -> "'$availableKey'" } ) + "]." } + /** + * Log message for Setting Evaluation errors when the method returns with empty value. The log eventId is 1002. + * + * @param methodName The method name where the error is logged. + * @param emptyResult The empty result. + * @return The formatted error message. + */ + fun getSettingEvaluationErrorWithEmptyValue(methodName: String, emptyResult: String): String { + return "Error occurred in the `$methodName` method. Returning $emptyResult." + } + + /** + * Log message for Setting Evaluation errors when the method returns with default value. The log eventId is 1002. + * + * @param methodName The method name where the error is logged. + * @param key The feature flag key. + * @param defaultParamName The default parameter name. + * @param defaultParamValue The default parameter value. + * @return The formatted error message. + */ + fun getSettingEvaluationErrorWithDefaultValue( + methodName: String, + key: String, + defaultParamName: String, + defaultParamValue: Any + ): String { + return "Error occurred in the `$methodName` method while evaluating setting '$key'. Returning the " + + "`$defaultParamName` parameter that you specified in your application: '$defaultParamValue'." + } + /** * Log message for Fetch Failed Due To Unexpected Http Response error. The log eventId is 1101. * @@ -127,9 +163,8 @@ internal object ConfigCatLogMessages { readTimeoutMillis: Long, writeTimeoutMillis: Long ): String { - return "Request timed out while trying to fetch config JSON. " + - "Timeout values: [connect: " + connectTimeoutMillis + "ms, " + - "read: " + readTimeoutMillis + "ms, write: " + writeTimeoutMillis + "ms]" + return "Request timed out while trying to fetch config JSON. Timeout values: [connect: " + + "${connectTimeoutMillis}ms, read: ${readTimeoutMillis}ms, write: ${writeTimeoutMillis}ms]" } /** @@ -155,17 +190,86 @@ internal object ConfigCatLogMessages { } /** - * Log message for Targeting Is Not Possible warning. The log eventId 3001. + * Log message for User Object is missing warning. The log eventId 3001. * * @param key The feature flag setting key. * @return The formatted warn message. */ - fun getTargetingIsNotPossible(key: String): String { + fun getUserObjectMissing(key: String): String { return "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()`/`getValueAsync()` " + + "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/" } + /** + * Log message for User Attribute is missing warning. The log eventId 3003. + * + * @param key The feature flag setting key. + * @param userCondition The user condition where the attribute is checked. + * @param attributeName The user attribute name. + * @return The formatted warn message. + */ + fun getUserAttributeMissing(key: String, userCondition: UserCondition, attributeName: String): String { + return "Cannot evaluate condition (${EvaluatorLogHelper.formatUserCondition(userCondition)}) 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/" + } + + /** + * Log message for User Attribute is missing warning. The log eventId 3003. + * + * @param key The feature flag setting key. + * @param attributeName The user attribute name. + * @return The formatted warn message. + */ + fun getUserAttributeMissing(key: String, attributeName: String): String { + return "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/" + } + + /** + * Log message for User Attribute is invalid warning. The log eventId 3004. + * + * @param key The feature flag setting key. + * @param userCondition The user condition where the attribute is checked. + * @param reason Why the attribute is invalid. + * @param attributeName The user attribute name. + * @return The formatted warn message. + */ + fun getUserAttributeInvalid( + key: String, + userCondition: UserCondition, + reason: String, + attributeName: String + ): String { + return "Cannot evaluate condition (${EvaluatorLogHelper.formatUserCondition(userCondition)}) for setting " + + "'$key' ($reason). Please check the User.$attributeName attribute and make sure that its value " + + "corresponds to the comparison operator." + } + + /** + * Log message for User Attribute value is automatically converted warning. The log eventId 3005. + * + * @param key The feature flag setting key. + * @param userCondition The condition where the circularity is detected. + * @param attributeName The user attribute name. + * @param attributeValue The user attribute value. + * @return The formatted warn message. + */ + fun getUserObjectAttributeIsAutoConverted( + key: String, + userCondition: UserCondition, + attributeName: String, + attributeValue: String + ): String { + return "Evaluation of condition (${EvaluatorLogHelper.formatUserCondition(userCondition)}) 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." + } + /** * Log message for Config Service Method Has No Effect Due To Closed Client warning. The log eventId 3201. * @@ -183,7 +287,7 @@ internal object ConfigCatLogMessages { * @return The formatted warn message. */ fun getAutoPollMaxInitWaitTimeReached(maxInitWaitTimeSeconds: Long): String { - return "`maxInitWaitTimeSeconds` for the very first fetch reached (" + maxInitWaitTimeSeconds + "s)." + + return "`maxInitWaitTimeSeconds` for the very first fetch reached (${maxInitWaitTimeSeconds}s)." + " Returning cached config." } diff --git a/src/commonMain/kotlin/com/configcat/model/Condition.kt b/src/commonMain/kotlin/com/configcat/model/Condition.kt new file mode 100644 index 00000000..962fe03b --- /dev/null +++ b/src/commonMain/kotlin/com/configcat/model/Condition.kt @@ -0,0 +1,17 @@ +package com.configcat.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Container class for different condition types. + */ +@Serializable +public data class Condition( + @SerialName(value = "u") + override val userCondition: UserCondition? = null, + @SerialName(value = "s") + override val segmentCondition: SegmentCondition? = null, + @SerialName(value = "p") + override val prerequisiteFlagCondition: PrerequisiteFlagCondition? = null +) : ConditionAccessor diff --git a/src/commonMain/kotlin/com/configcat/model/ConditionAccessor.kt b/src/commonMain/kotlin/com/configcat/model/ConditionAccessor.kt new file mode 100644 index 00000000..8dbff478 --- /dev/null +++ b/src/commonMain/kotlin/com/configcat/model/ConditionAccessor.kt @@ -0,0 +1,10 @@ +package com.configcat.model + +import kotlinx.serialization.Serializable + +@Serializable +internal sealed interface ConditionAccessor { + val userCondition: UserCondition? + val segmentCondition: SegmentCondition? + val prerequisiteFlagCondition: PrerequisiteFlagCondition? +} diff --git a/src/commonMain/kotlin/com/configcat/model/Config.kt b/src/commonMain/kotlin/com/configcat/model/Config.kt new file mode 100644 index 00000000..7fce2ab0 --- /dev/null +++ b/src/commonMain/kotlin/com/configcat/model/Config.kt @@ -0,0 +1,32 @@ +package com.configcat.model + +import kotlinx.serialization.* +import kotlinx.serialization.json.* + +/** + * ConfigCat config. + */ +@Serializable +public data class Config( + /** + * The config preferences. + */ + @SerialName("p") + val preferences: Preferences?, + /** + * Map of flags / settings. + */ + @SerialName("f") + var settings: Map? = null, + /** + * List of segments. + */ + @SerialName("s") + var segments: Array? = null +) { + internal fun isEmpty(): Boolean = this == empty + + internal companion object { + val empty: Config = Config(null, mapOf(), arrayOf()) + } +} diff --git a/src/commonMain/kotlin/com/configcat/model/Entry.kt b/src/commonMain/kotlin/com/configcat/model/Entry.kt new file mode 100644 index 00000000..96527951 --- /dev/null +++ b/src/commonMain/kotlin/com/configcat/model/Entry.kt @@ -0,0 +1,45 @@ +package com.configcat.model + +import com.configcat.Constants +import com.configcat.DateTimeUtils +import com.configcat.Helpers +import com.soywiz.klock.DateTime + +internal data class Entry( + val config: Config, + val eTag: String, + val configJson: String, + val fetchTime: DateTime +) { + fun isEmpty(): Boolean = this === empty + + companion object { + val empty: Entry = Entry(Config.empty, "", "", Constants.distantPast) + + fun fromString(cacheValue: String?): Entry { + if (cacheValue.isNullOrEmpty()) { + return empty + } + val fetchTimeIndex = cacheValue.indexOf("\n") + val eTagIndex = cacheValue.indexOf("\n", fetchTimeIndex + 1) + require(fetchTimeIndex > 0 && eTagIndex > 0) { "Number of values is fewer than expected." } + val fetchTimeRaw = cacheValue.substring(0, fetchTimeIndex) + require(DateTimeUtils.isValidDate(fetchTimeRaw)) { "Invalid fetch time: $fetchTimeRaw" } + val fetchTimeUnixMillis = fetchTimeRaw.toLong() + val eTag = cacheValue.substring(fetchTimeIndex + 1, eTagIndex) + require(eTag.isNotEmpty()) { "Empty eTag value." } + val configJson = cacheValue.substring(eTagIndex + 1) + require(configJson.isNotEmpty()) { "Empty config jsom value." } + return try { + val config: Config = Helpers.parseConfigJson(configJson) + Entry(config, eTag, configJson, DateTime(fetchTimeUnixMillis)) + } catch (e: Exception) { + throw IllegalArgumentException("Invalid config JSON content: $configJson", e) + } + } + } + + fun serialize(): String { + return "${fetchTime.unixMillis.toLong()}\n${eTag}\n$configJson" + } +} diff --git a/src/commonMain/kotlin/com/configcat/model/PercentageOption.kt b/src/commonMain/kotlin/com/configcat/model/PercentageOption.kt new file mode 100644 index 00000000..5aa9a4c4 --- /dev/null +++ b/src/commonMain/kotlin/com/configcat/model/PercentageOption.kt @@ -0,0 +1,28 @@ +package com.configcat.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Percentage option. + */ +@Serializable +public data class PercentageOption( + /** + * A number between 0 and 100 that represents a randomly allocated fraction of the users. + */ + @SerialName(value = "p") + val percentage: Int = 0, + /** + * The server value of the percentage option. + */ + @SerialName(value = "v") + val value: SettingValue, + /** + * The variation ID of the percentage option. + */ + @SerialName(value = "i") + val variationId: String? = null +) { + // No implementation +} diff --git a/src/commonMain/kotlin/com/configcat/model/Preferences.kt b/src/commonMain/kotlin/com/configcat/model/Preferences.kt new file mode 100644 index 00000000..811ae1ba --- /dev/null +++ b/src/commonMain/kotlin/com/configcat/model/Preferences.kt @@ -0,0 +1,20 @@ +package com.configcat.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * The config preferences. + */ +@Serializable +public data class Preferences( + @SerialName(value = "u") + val baseUrl: String, + @SerialName(value = "r") + val redirect: Int = 0, + /** + * The config salt which was used to hash sensitive data. + */ + @SerialName(value = "s") + val salt: String? +) diff --git a/src/commonMain/kotlin/com/configcat/model/PrerequisiteFlagCondition.kt b/src/commonMain/kotlin/com/configcat/model/PrerequisiteFlagCondition.kt new file mode 100644 index 00000000..75bd69b1 --- /dev/null +++ b/src/commonMain/kotlin/com/configcat/model/PrerequisiteFlagCondition.kt @@ -0,0 +1,29 @@ +package com.configcat.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Prerequisite Flag Condition. + */ +@Serializable +public data class PrerequisiteFlagCondition( + /** + * The key of the prerequisite flag that the condition is based on. + */ + @SerialName(value = "f") + val prerequisiteFlagKey: String? = null, + /** + * The operator which defines the relation between the evaluated value of the prerequisite flag and + * the comparison value. + */ + @SerialName(value = "c") + val prerequisiteComparator: Int = -1, + /** + * The value that the evaluated value of the prerequisite flag is compared to. + */ + @SerialName(value = "v") + val value: SettingValue? = null +) { + // No implementation +} diff --git a/src/commonMain/kotlin/com/configcat/model/Segment.kt b/src/commonMain/kotlin/com/configcat/model/Segment.kt new file mode 100644 index 00000000..0735c7cd --- /dev/null +++ b/src/commonMain/kotlin/com/configcat/model/Segment.kt @@ -0,0 +1,24 @@ +package com.configcat.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * ConfigCat segment. + */ +@Serializable +public data class Segment( + /** + * The name of the segment. + */ + @SerialName("n") + val name: String? = null, + /** + * The list of segment rule conditions (where there is a logical AND relation between the items). + */ + @SerialName("r") + val segmentRules: Array? = null +) { + internal val conditionAccessors: List = + segmentRules?.let { condition -> condition.map { it } } ?: listOf() +} diff --git a/src/commonMain/kotlin/com/configcat/model/SegmentCondition.kt b/src/commonMain/kotlin/com/configcat/model/SegmentCondition.kt new file mode 100644 index 00000000..e6175915 --- /dev/null +++ b/src/commonMain/kotlin/com/configcat/model/SegmentCondition.kt @@ -0,0 +1,23 @@ +package com.configcat.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Segment Condition. + */ +@Serializable +public data class SegmentCondition( + /** + * The index of the segment that the condition is based on. + */ + @SerialName(value = "s") + val segmentIndex: Int = -1, + /** + * The operator which defines the expected result of the evaluation of the segment. + */ + @SerialName(value = "c") + val segmentComparator: Int = -1 +) { + // No implementation +} diff --git a/src/commonMain/kotlin/com/configcat/model/Setting.kt b/src/commonMain/kotlin/com/configcat/model/Setting.kt new file mode 100644 index 00000000..060c73d7 --- /dev/null +++ b/src/commonMain/kotlin/com/configcat/model/Setting.kt @@ -0,0 +1,50 @@ +package com.configcat.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** Describes a feature flag / setting. */ +@Serializable +public data class Setting( + /** + * Type of the feature flag / setting. + * + * 0 -> [Boolean], + * 1 -> [String], + * 2 -> [Int], + * 3 -> [Double], + */ + @SerialName(value = "t") + var type: Int = -1, + /** + * The User Object attribute which serves as the basis of percentage options evaluation. + */ + @SerialName(value = "a") + val percentageAttribute: String? = null, + /** + * The list of percentage options. + */ + @SerialName(value = "p") + val percentageOptions: Array? = null, + /** + * The list of targeting rules (where there is a logical OR relation between the items). + */ + @SerialName(value = "r") + val targetingRules: Array? = null, + /** + * The value of the setting. + */ + @SerialName(value = "v") + val settingValue: SettingValue, + /** + * The variation ID of the setting. + */ + @SerialName(value = "i") + val variationId: String? = null +) { + + var configSalt: String? = null + internal var segments: Array = arrayOf() + + public constructor() : this(0, "", null, null, SettingValue(), "") +} diff --git a/src/commonMain/kotlin/com/configcat/model/SettingType.kt b/src/commonMain/kotlin/com/configcat/model/SettingType.kt new file mode 100644 index 00000000..5b2f46ed --- /dev/null +++ b/src/commonMain/kotlin/com/configcat/model/SettingType.kt @@ -0,0 +1,14 @@ +package com.configcat.model + +/** + * Describes the type of ConfigCat Feature Flag / Setting. + */ +public enum class SettingType(public val id: Int, public val value: String) { + BOOLEAN(0, "Boolean"), + STRING(1, "String"), + INT(2, "Int"), + DOUBLE(3, "Double"), + + // This is only used by the client, never presented to the user + JS_NUMBER(-1, "JsNumber") +} diff --git a/src/commonMain/kotlin/com/configcat/model/SettingValue.kt b/src/commonMain/kotlin/com/configcat/model/SettingValue.kt new file mode 100644 index 00000000..31231865 --- /dev/null +++ b/src/commonMain/kotlin/com/configcat/model/SettingValue.kt @@ -0,0 +1,74 @@ +package com.configcat.model + +import com.configcat.Client.SettingTypeHelper.toSettingTypeOrNull +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Setting Value contains the proper value based on type. + */ +@Serializable +public data class SettingValue( + @SerialName("b") + var booleanValue: Boolean? = null, + + @SerialName("s") + var stringValue: String? = null, + + @SerialName("i") + var integerValue: Int? = null, + + @SerialName("d") + var doubleValue: Double? = null +) { + internal fun equalsBasedOnSettingType(other: Any?, settingType: Int): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as SettingValue + val settingTypeEnum = settingType.toSettingTypeOrNull() + return when (settingTypeEnum) { + SettingType.BOOLEAN -> { + booleanValue == other.booleanValue + } + + SettingType.STRING -> { + stringValue == other.stringValue + } + + SettingType.INT -> { + integerValue == other.integerValue + } + + SettingType.DOUBLE -> { + doubleValue == other.doubleValue + } + + SettingType.JS_NUMBER -> { + (doubleValue ?: integerValue?.toDouble()) == (other.doubleValue ?: other.integerValue?.toDouble()) + } + + else -> { + throw IllegalArgumentException( + "Setting is of an unsupported type ($settingTypeEnum)." + ) + } + } + } + + override fun toString(): String { + return when { + booleanValue != null -> { + booleanValue.toString() + } + integerValue != null -> { + integerValue.toString() + } + doubleValue != null -> { + doubleValue.toString() + } else -> { + stringValue.toString() + } + } + } +} diff --git a/src/commonMain/kotlin/com/configcat/model/TargetingRule.kt b/src/commonMain/kotlin/com/configcat/model/TargetingRule.kt new file mode 100644 index 00000000..e46ef324 --- /dev/null +++ b/src/commonMain/kotlin/com/configcat/model/TargetingRule.kt @@ -0,0 +1,41 @@ +package com.configcat.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Targeting rule. + */ +@Serializable +public data class TargetingRule( + /** + * The list of conditions (where there is a logical AND relation between the items). + */ + @SerialName(value = "c") + val conditions: Array? = null, + /** + * The list of percentage options associated with the targeting rule or {@code null} if the targeting rule + * has a simple value THEN part. + */ + @SerialName(value = "p") + val percentageOptions: Array? = null, + /** + * The value associated with the targeting rule or {@code null} if the targeting rule has percentage options + * THEN part. + */ + @SerialName(value = "s") + val servedValue: ServedValue? = null +) { + internal val conditionAccessors: List = + conditions?.let { condition -> condition.map { it } } ?: listOf() +} + +@Serializable +public data class ServedValue( + @SerialName(value = "v") + val value: SettingValue, + @SerialName(value = "i") + val variationId: String? = null +) { + // No implementation +} diff --git a/src/commonMain/kotlin/com/configcat/model/UserCondition.kt b/src/commonMain/kotlin/com/configcat/model/UserCondition.kt new file mode 100644 index 00000000..9a355ce9 --- /dev/null +++ b/src/commonMain/kotlin/com/configcat/model/UserCondition.kt @@ -0,0 +1,47 @@ +package com.configcat.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient + +/** + * User Condition. + */ +@Serializable +public data class UserCondition( + /** + * The User Object attribute that the condition is based on. Can be "User ID", "Email", "Country" or + * any custom attribute. + */ + @SerialName("a") + val comparisonAttribute: String, + /** + * The operator which defines the relation between the comparison attribute and the comparison value. + */ + @SerialName("c") + val comparator: Int = -1, + /** + * The String value that the attribute is compared or {@code null} if the comparator use a different type. + */ + @SerialName("s") + val stringValue: String? = null, + /** + * The Double value that the attribute is compared or {@code null} if the comparator use a different type. + */ + @SerialName("d") + val doubleValue: Double? = null, + /** + * The String Array value that the attribute is compared or {@code null} if the comparator use a different type. + */ + @SerialName("l") + val stringArrayValue: Array? = null +) : ConditionAccessor { + @Transient + override val userCondition: UserCondition = this + + @Transient + override val segmentCondition: SegmentCondition? = null + + @Transient + override val prerequisiteFlagCondition: PrerequisiteFlagCondition? = null +} diff --git a/src/commonMain/kotlin/com/configcat/override/DataSource.kt b/src/commonMain/kotlin/com/configcat/override/DataSource.kt index b5200ae4..73a2d856 100644 --- a/src/commonMain/kotlin/com/configcat/override/DataSource.kt +++ b/src/commonMain/kotlin/com/configcat/override/DataSource.kt @@ -1,6 +1,8 @@ package com.configcat.override -import com.configcat.Setting +import com.configcat.Helpers +import com.configcat.model.Config +import com.configcat.model.Setting /** * Describes a data source for [FlagOverrides]. @@ -19,25 +21,55 @@ public interface OverrideDataSource { * Create an [OverrideDataSource] that stores the overrides in a key-value map. */ public fun map(map: Map): OverrideDataSource { - return MapOverrideDataSource(map.map { it.key to Setting(it.value) }.toMap()) + return MapOverrideDataSource(map.map { it.key to convertToSetting(it.value) }.toMap()) } /** - * Create an [OverrideDataSource] that stores the override settings in a key-value map. + * Create an [OverrideDataSource] that stores the override config in a key-value map. */ - public fun settings(map: Map): OverrideDataSource { - return SettingsOverrideDataSource(map) + public fun config(config: Config): OverrideDataSource { + return ConfigOverrideDataSource(config) } } } +internal expect fun convertToSetting(value: Any): Setting + +internal fun commonConvertToSetting(value: Any): Setting { + val setting = Setting() + when (value) { + is Boolean -> { + setting.settingValue.booleanValue = value + setting.type = 0 + } + + is Int -> { + setting.settingValue.integerValue = value + setting.type = 2 + } + + is Double -> { + setting.settingValue.doubleValue = value + setting.type = 3 + } + + else -> { + setting.settingValue.stringValue = value.toString() + setting.type = 1 + } + } + return setting +} + internal class MapOverrideDataSource constructor(private val map: Map) : OverrideDataSource { override fun getOverrides(): Map { return map } } -internal class SettingsOverrideDataSource constructor(private val map: Map) : OverrideDataSource { + +internal class ConfigOverrideDataSource constructor(private val config: Config) : OverrideDataSource { override fun getOverrides(): Map { - return map + Helpers.addConfigSaltAndSegmentsToSettings(config) + return config.settings ?: emptyMap() } } diff --git a/src/commonTest/kotlin/com/configcat/CommonUtilsTests.kt b/src/commonTest/kotlin/com/configcat/CommonUtilsTests.kt new file mode 100644 index 00000000..381ff8fc --- /dev/null +++ b/src/commonTest/kotlin/com/configcat/CommonUtilsTests.kt @@ -0,0 +1,138 @@ +package com.configcat + +import com.configcat.model.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.Serializable +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonPrimitive +import kotlin.test.* + +@OptIn(ExperimentalCoroutinesApi::class) +class CommonUtilsTests { + + @Test + fun testValidateSettingValueType() = runTest { + val settingValue = SettingValue(true, "stringValue", 1, 3.14) + // test valid types -1, 0, 1, 2, 3 + var result: Any = Helpers.validateSettingValueType(settingValue, -1) + assertEquals(3.14, result) + result = Helpers.validateSettingValueType(settingValue, 0) + assertEquals(true, result) + result = Helpers.validateSettingValueType(settingValue, 1) + assertEquals("stringValue", result) + result = Helpers.validateSettingValueType(settingValue, 2) + assertEquals(1, result) + result = Helpers.validateSettingValueType(settingValue, 3) + assertEquals(3.14, result) + + // test setting value not matching type - bool value & string type + val invalidSettingValue = SettingValue(true, null, null, null) + val invalidSettingResultException = assertFailsWith( + exceptionClass = IllegalArgumentException::class, + block = { Helpers.validateSettingValueType(invalidSettingValue, 1) } + ) + assertEquals("Setting value is not of the expected type String.", invalidSettingResultException.message) + + // test invalid type - 99 + val invalidTypeException = assertFailsWith( + exceptionClass = IllegalArgumentException::class, + block = { Helpers.validateSettingValueType(settingValue, 99) } + ) + assertEquals("Setting is of an unsupported type (null).", invalidTypeException.message) + + // test null setting value + val invalidSettingValueException = assertFailsWith( + exceptionClass = IllegalArgumentException::class, + block = { Helpers.validateSettingValueType(null, 1) } + ) + assertEquals("Setting value is missing or invalid.", invalidSettingValueException.message) + } + + @Test + fun testAddConfigSaltAndSegmentsToSettings() = runTest { + val config = Config( + Preferences("", 0, "test-salt"), + mapOf( + "test-setting" to Setting( + 1, + "", + null, + null, + SettingValue(stringValue = "noRule"), + "myVariationId" + ) + ), + arrayOf(Segment("test-segment", null)) + ) + Helpers.addConfigSaltAndSegmentsToSettings(config) + + assertEquals("test-salt", config.settings?.get("test-setting")?.configSalt) + assertEquals("test-segment", config.settings?.get("test-setting")?.segments?.get(0)?.name) + } + + @Test + fun testParseConfigJson() = runTest { + val configResult = Helpers.parseConfigJson(Data.formatJsonBodyWithString("fake")) + assertNotNull(configResult) + assertEquals("test-salt", configResult.preferences?.salt) + assertEquals("https://cdn-global.configcat.com", configResult.preferences?.baseUrl) + assertEquals(1, configResult.settings?.size) + assertEquals("fake", configResult.settings?.get("fakeKey")?.settingValue?.stringValue) + } + + @Test + fun testFlagValueSerializer() = runTest { + var obj = SerializeTestClass("testWithString") + var encoded = Json.encodeToString(obj) + assertEquals("{\"testData\":\"testWithString\"}", encoded) + var decoded = Json.decodeFromString(encoded) + assertEquals(obj, decoded) + + obj = SerializeTestClass(1) + encoded = Json.encodeToString(obj) + assertEquals("{\"testData\":1}", encoded) + decoded = Json.decodeFromString(encoded) + assertEquals(obj, decoded) + + obj = SerializeTestClass(true) + encoded = Json.encodeToString(obj) + assertEquals("{\"testData\":true}", encoded) + decoded = Json.decodeFromString(encoded) + assertEquals(obj, decoded) + + obj = SerializeTestClass(JsonPrimitive("testJsonElement")) + encoded = Json.encodeToString(obj) + assertEquals("{\"testData\":\"testJsonElement\"}", encoded) + decoded = Json.decodeFromString(encoded) + assertEquals("testJsonElement", decoded.testData) + + // test fails + val failObject = ConfigCatUser("test") + val serializeException = assertFailsWith( + exceptionClass = IllegalArgumentException::class, + block = { + obj = SerializeTestClass(failObject) + encoded = Json.encodeToString(obj) + } + ) + assertEquals("Unable to encode $failObject", serializeException.message) + + val failDecodeString = "{\"testData\":{\"testData2\":\"testJsonElement\"}}" + val decodeException = assertFailsWith( + exceptionClass = IllegalStateException::class, + block = { + decoded = Json.decodeFromString(failDecodeString) + } + ) + assertEquals("Unable to decode {\"testData2\":\"testJsonElement\"}", decodeException.message) + } + + @Serializable + data class SerializeTestClass( + @Serializable(with = Constants.FlagValueSerializer::class) + val testData: Any + ) +} diff --git a/src/commonTest/kotlin/com/configcat/ConfigCatClientTests.kt b/src/commonTest/kotlin/com/configcat/ConfigCatClientTests.kt index 5ff55de5..32d280e0 100644 --- a/src/commonTest/kotlin/com/configcat/ConfigCatClientTests.kt +++ b/src/commonTest/kotlin/com/configcat/ConfigCatClientTests.kt @@ -1,10 +1,14 @@ package com.configcat +import com.configcat.evaluation.EvaluationTestLogger +import com.configcat.evaluation.LogEvent +import com.configcat.log.LogLevel import com.configcat.override.OverrideBehavior import com.soywiz.klock.DateTime import com.soywiz.krypto.sha1 import io.ktor.client.engine.mock.* import io.ktor.http.* +import io.ktor.util.* import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -25,11 +29,11 @@ class ConfigCatClientTests { fun testGetValue() = runTest { val mockEngine = MockEngine { respond( - content = Data.formatJsonBody(true), + content = Data.formatJsonBodyWithBoolean(true), status = HttpStatusCode.OK ) } - val client = ConfigCatClient("test") { + val client = ConfigCatClient(Data.SDK_KEY) { httpEngine = mockEngine } val op1 = async { client.getValue("fakeKey", false) } @@ -47,11 +51,11 @@ class ConfigCatClientTests { fun testGetIntValue() = runTest { val mockEngine = MockEngine { respond( - content = Data.formatJsonBody(10), + content = Data.formatJsonBodyWithInt(10), status = HttpStatusCode.OK ) } - val client = ConfigCatClient("test") { + val client = ConfigCatClient(Data.SDK_KEY) { httpEngine = mockEngine } @@ -67,7 +71,7 @@ class ConfigCatClientTests { status = HttpStatusCode.BadGateway ) } - val client = ConfigCatClient("test") { + val client = ConfigCatClient(Data.SDK_KEY) { httpEngine = mockEngine } @@ -79,16 +83,39 @@ class ConfigCatClientTests { fun testGetValueTypeMismatch() = runTest { val mockEngine = MockEngine { respond( - content = Data.formatJsonBody("fake"), + content = Data.formatJsonBodyWithString("fake"), status = HttpStatusCode.OK ) } - val client = ConfigCatClient("test") { + + val evaluationTestLogger = EvaluationTestLogger() + + val client = ConfigCatClient(Data.SDK_KEY) { httpEngine = mockEngine + logLevel = LogLevel.ERROR + logger = evaluationTestLogger + configCache = SingleValueCache("") } - assertEquals(0, client.getValue("fakeKey", 0)) - assertEquals(1, mockEngine.requestHistory.size) + val result = client.getValue("fakeKey", 0) + assertEquals(0, result) + + val errorLogs = mutableListOf() + + val logsList = evaluationTestLogger.getLogList() + for (i in logsList.indices) { + val log = logsList[i] + if (log.logLevel == LogLevel.ERROR) { + errorLogs.add(log) + } + } + assertEquals(1, errorLogs.size, "Error size not matching") + val errorMessage: String = errorLogs[0].logMessage + assertContains(errorMessage, "[1002]") + assertContains(errorMessage, "Error occurred in the `getAnyValue` method while evaluating setting 'fakeKey'. Returning the `defaultValue` parameter that you specified in your application: '0'.") + // we don't check the full exception message because the Integer class can be different in other platforms. We only check the first part of the message + assertContains(errorMessage, "The type of a setting must match the type of the specified default value. Setting's type was {STRING} but the default value's type was ") + evaluationTestLogger.resetLogList() } @Test @@ -99,7 +126,7 @@ class ConfigCatClientTests { status = HttpStatusCode.OK ) } - val client = ConfigCatClient("test") { + val client = ConfigCatClient(Data.SDK_KEY) { httpEngine = mockEngine } @@ -115,7 +142,7 @@ class ConfigCatClientTests { status = HttpStatusCode.BadRequest ) } - val client = ConfigCatClient("test") { + val client = ConfigCatClient(Data.SDK_KEY) { httpEngine = mockEngine } @@ -127,11 +154,11 @@ class ConfigCatClientTests { fun testGetStringValue() = runTest { val mockEngine = MockEngine { respond( - content = Data.formatJsonBody("test"), + content = Data.formatJsonBodyWithString("test"), status = HttpStatusCode.OK ) } - val client = ConfigCatClient("test") { + val client = ConfigCatClient(Data.SDK_KEY) { httpEngine = mockEngine } @@ -147,7 +174,7 @@ class ConfigCatClientTests { status = HttpStatusCode.BadGateway ) } - val client = ConfigCatClient("test") { + val client = ConfigCatClient(Data.SDK_KEY) { httpEngine = mockEngine } @@ -159,11 +186,11 @@ class ConfigCatClientTests { fun testGetDoubleValue() = runTest { val mockEngine = MockEngine { respond( - content = Data.formatJsonBody(3.14), + content = Data.formatJsonBodyWithDouble(3.14), status = HttpStatusCode.OK ) } - val client = ConfigCatClient("test") { + val client = ConfigCatClient(Data.SDK_KEY) { httpEngine = mockEngine } @@ -179,7 +206,7 @@ class ConfigCatClientTests { status = HttpStatusCode.BadGateway ) } - val client = ConfigCatClient("test") { + val client = ConfigCatClient(Data.SDK_KEY) { httpEngine = mockEngine } @@ -191,11 +218,11 @@ class ConfigCatClientTests { fun testGetBoolValue() = runTest { val mockEngine = MockEngine { respond( - content = Data.formatJsonBody(true), + content = Data.formatJsonBodyWithBoolean(true), status = HttpStatusCode.OK ) } - val client = ConfigCatClient("test") { + val client = ConfigCatClient(Data.SDK_KEY) { httpEngine = mockEngine } @@ -211,7 +238,7 @@ class ConfigCatClientTests { status = HttpStatusCode.BadGateway ) } - val client = ConfigCatClient("test") { + val client = ConfigCatClient(Data.SDK_KEY) { httpEngine = mockEngine } @@ -219,29 +246,13 @@ class ConfigCatClientTests { assertEquals(1, mockEngine.requestHistory.size) } - @Test - fun testGetValueInvalidType() = runTest { - val mockEngine = MockEngine { - respond( - content = Data.formatJsonBody(true), - status = HttpStatusCode.OK - ) - } - val client = ConfigCatClient("test") { - httpEngine = mockEngine - } - - assertEquals("55".toFloat(), client.getValue("fakeKey", "55".toFloat())) - assertEquals(1, mockEngine.requestHistory.size) - } - @Test fun testRequestTimeout() = runTest { val mockEngine = MockEngine { delay(3000) - respond(content = Data.formatJsonBody(true), status = HttpStatusCode.OK) + respond(content = Data.formatJsonBodyWithBoolean(true), status = HttpStatusCode.OK) } - val client = ConfigCatClient("test") { + val client = ConfigCatClient(Data.SDK_KEY) { httpEngine = mockEngine requestTimeout = 1.seconds } @@ -261,10 +272,12 @@ class ConfigCatClientTests { status = HttpStatusCode.BadRequest ) } - val sdkKey = "test" - val cacheKey: String = "${sdkKey}_${Constants.configFileName}_${Constants.serializationFormatVersion}".encodeToByteArray().sha1().hex + val sdkKey = Data.SDK_KEY + val cacheKey: String = + "${sdkKey}_${Constants.configFileName}_${Constants.serializationFormatVersion}".encodeToByteArray() + .sha1().hex val cache = InMemoryCache() - cache.write(cacheKey, Data.formatCacheEntry(true)) + cache.write(cacheKey, Data.formatCacheEntry("test")) val client = ConfigCatClient(sdkKey) { httpEngine = mockEngine configCache = cache @@ -273,7 +286,7 @@ class ConfigCatClientTests { } } - assertEquals(true, client.getValue("fakeKey", false)) + assertEquals("test", client.getValue("fakeKey", "")) TestUtils.awaitUntil { mockEngine.requestHistory.size == 1 @@ -288,10 +301,12 @@ class ConfigCatClientTests { status = HttpStatusCode.NotFound ) } - val sdkKey = "test" - val cacheKey: String = "${sdkKey}_${Constants.configFileName}_${Constants.serializationFormatVersion}".encodeToByteArray().sha1().hex + val sdkKey = Data.SDK_KEY + val cacheKey: String = + "${sdkKey}_${Constants.configFileName}_${Constants.serializationFormatVersion}".encodeToByteArray() + .sha1().hex val cache = InMemoryCache() - cache.write(cacheKey, Data.formatCacheEntry(true)) + cache.write(cacheKey, Data.formatCacheEntry("test")) val client = ConfigCatClient(sdkKey) { httpEngine = mockEngine configCache = cache @@ -299,8 +314,11 @@ class ConfigCatClientTests { val result = client.forceRefresh() assertFalse(result.isSuccess) - assertEquals("Your SDK Key seems to be wrong. You can find the valid SDK Key at https://app.configcat.com/sdkkey. Received response: 404 Not Found", result.error) - assertEquals(true, client.getValue("fakeKey", false)) + assertEquals( + "Your SDK Key seems to be wrong. You can find the valid SDK Key at https://app.configcat.com/sdkkey. Received response: 404 Not Found", + result.error + ) + assertEquals("test", client.getValue("fakeKey", "")) assertTrue(mockEngine.requestHistory.size == 1 || mockEngine.requestHistory.size == 2) } @@ -308,20 +326,23 @@ class ConfigCatClientTests { fun testGetLatestOnFail() = runTest { val mockEngine = MockEngine.create { this.addHandler { - respond(content = Data.formatJsonBody("test1"), status = HttpStatusCode.OK) + respond(content = Data.formatJsonBodyWithString("test1"), status = HttpStatusCode.OK) } this.addHandler { respond(content = "", status = HttpStatusCode.Forbidden) } } as MockEngine - val client = ConfigCatClient("test") { + val client = ConfigCatClient(Data.SDK_KEY) { httpEngine = mockEngine } assertEquals("test1", client.getValue("fakeKey", "")) val result = client.forceRefresh() assertFalse(result.isSuccess) - assertEquals("Your SDK Key seems to be wrong. You can find the valid SDK Key at https://app.configcat.com/sdkkey. Received response: 403 Forbidden", result.error) + assertEquals( + "Your SDK Key seems to be wrong. You can find the valid SDK Key at https://app.configcat.com/sdkkey. Received response: 403 Forbidden", + result.error + ) assertEquals("test1", client.getValue("fakeKey", "")) assertEquals(2, mockEngine.requestHistory.size) } @@ -330,13 +351,13 @@ class ConfigCatClientTests { fun testForceRefreshLazy() = runTest { val mockEngine = MockEngine.create { this.addHandler { - respond(content = Data.formatJsonBody("test1"), status = HttpStatusCode.OK) + respond(content = Data.formatJsonBodyWithString("test1"), status = HttpStatusCode.OK) } this.addHandler { - respond(content = Data.formatJsonBody("test2"), status = HttpStatusCode.OK) + respond(content = Data.formatJsonBodyWithString("test2"), status = HttpStatusCode.OK) } } as MockEngine - val client = ConfigCatClient("test") { + val client = ConfigCatClient(Data.SDK_KEY) { httpEngine = mockEngine pollingMode = lazyLoad() } @@ -352,13 +373,13 @@ class ConfigCatClientTests { fun testForceRefreshAuto() = runTest { val mockEngine = MockEngine.create { this.addHandler { - respond(content = Data.formatJsonBody("test1"), status = HttpStatusCode.OK) + respond(content = Data.formatJsonBodyWithString("test1"), status = HttpStatusCode.OK) } this.addHandler { - respond(content = Data.formatJsonBody("test2"), status = HttpStatusCode.OK) + respond(content = Data.formatJsonBodyWithString("test2"), status = HttpStatusCode.OK) } } as MockEngine - val client = ConfigCatClient("test") { + val client = ConfigCatClient(Data.SDK_KEY) { httpEngine = mockEngine pollingMode = autoPoll() } @@ -378,7 +399,7 @@ class ConfigCatClientTests { status = HttpStatusCode.BadRequest ) } - val client = ConfigCatClient("test") { + val client = ConfigCatClient(Data.SDK_KEY) { httpEngine = mockEngine } @@ -394,13 +415,16 @@ class ConfigCatClientTests { status = HttpStatusCode.NotFound ) } - val client = ConfigCatClient("test") { + val client = ConfigCatClient(Data.SDK_KEY) { httpEngine = mockEngine } val result = client.forceRefresh() assertFalse(result.isSuccess) - assertEquals("Your SDK Key seems to be wrong. You can find the valid SDK Key at https://app.configcat.com/sdkkey. Received response: 404 Not Found", result.error) + assertEquals( + "Your SDK Key seems to be wrong. You can find the valid SDK Key at https://app.configcat.com/sdkkey. Received response: 404 Not Found", + result.error + ) assertEquals(false, client.getValue("fakeKey", false)) assertTrue(mockEngine.requestHistory.size == 1 || mockEngine.requestHistory.size == 2) } @@ -413,7 +437,7 @@ class ConfigCatClientTests { status = HttpStatusCode.BadRequest ) } - val client = ConfigCatClient("test") { + val client = ConfigCatClient(Data.SDK_KEY) { httpEngine = mockEngine pollingMode = lazyLoad() } @@ -430,14 +454,17 @@ class ConfigCatClientTests { status = HttpStatusCode.NotFound ) } - val client = ConfigCatClient("test") { + val client = ConfigCatClient(Data.SDK_KEY) { httpEngine = mockEngine pollingMode = lazyLoad() } val result = client.forceRefresh() assertFalse(result.isSuccess) - assertEquals("Your SDK Key seems to be wrong. You can find the valid SDK Key at https://app.configcat.com/sdkkey. Received response: 404 Not Found", result.error) + assertEquals( + "Your SDK Key seems to be wrong. You can find the valid SDK Key at https://app.configcat.com/sdkkey. Received response: 404 Not Found", + result.error + ) assertEquals(false, client.getValue("fakeKey", false)) assertEquals(2, mockEngine.requestHistory.size) } @@ -446,11 +473,11 @@ class ConfigCatClientTests { fun testGetAllKeys() = runTest { val mockEngine = MockEngine { respond( - content = testMultipleBody, + content = Data.MULTIPLE_BODY, status = HttpStatusCode.OK ) } - val client = ConfigCatClient("test") { + val client = ConfigCatClient(Data.SDK_KEY) { httpEngine = mockEngine } @@ -462,11 +489,11 @@ class ConfigCatClientTests { fun testGetAllValues() = runTest { val mockEngine = MockEngine { respond( - content = testMultipleBody, + content = Data.MULTIPLE_BODY, status = HttpStatusCode.OK ) } - val client = ConfigCatClient("test") { + val client = ConfigCatClient(Data.SDK_KEY) { httpEngine = mockEngine } @@ -480,11 +507,11 @@ class ConfigCatClientTests { fun testGetAllValueDetails() = runTest { val mockEngine = MockEngine { respond( - content = testMultipleBody, + content = Data.MULTIPLE_BODY, status = HttpStatusCode.OK ) } - val client = ConfigCatClient("test") { + val client = ConfigCatClient(Data.SDK_KEY) { httpEngine = mockEngine } @@ -498,60 +525,84 @@ class ConfigCatClientTests { fun testAutoPollUserAgent() = runTest { val mockEngine = MockEngine { respond( - content = Data.formatJsonBody(true), + content = Data.formatJsonBodyWithBoolean(true), status = HttpStatusCode.OK ) } - val client = ConfigCatClient("test") { + val client = ConfigCatClient(Data.SDK_KEY) { httpEngine = mockEngine pollingMode = autoPoll() } client.getValue("fakeKey", false) - assertEquals( - "ConfigCat-Kotlin/a-${Constants.version}", - mockEngine.requestHistory.last().headers["X-ConfigCat-UserAgent"] - ) + // For Js we check the query params + if (PlatformUtils.IS_BROWSER || PlatformUtils.IS_NODE) { + assertEquals( + "ConfigCat-Kotlin/a-${Constants.version}", + mockEngine.requestHistory.last().url.parameters["sdk"] + ) + } else { + assertEquals( + "ConfigCat-Kotlin/a-${Constants.version}", + mockEngine.requestHistory.last().headers["X-ConfigCat-UserAgent"] + ) + } } @Test fun testLazyUserAgent() = runTest { val mockEngine = MockEngine { respond( - content = Data.formatJsonBody(true), + content = Data.formatJsonBodyWithBoolean(true), status = HttpStatusCode.OK ) } - val client = ConfigCatClient("test") { + val client = ConfigCatClient(Data.SDK_KEY) { httpEngine = mockEngine pollingMode = lazyLoad() } client.getValue("fakeKey", false) - assertEquals( - "ConfigCat-Kotlin/l-${Constants.version}", - mockEngine.requestHistory.last().headers["X-ConfigCat-UserAgent"] - ) + // For Js we check the query params + if (PlatformUtils.IS_BROWSER || PlatformUtils.IS_NODE) { + assertEquals( + "ConfigCat-Kotlin/l-${Constants.version}", + mockEngine.requestHistory.last().url.parameters["sdk"] + ) + } else { + assertEquals( + "ConfigCat-Kotlin/l-${Constants.version}", + mockEngine.requestHistory.last().headers["X-ConfigCat-UserAgent"] + ) + } } @Test fun testManualPollUserAgent() = runTest { val mockEngine = MockEngine { respond( - content = Data.formatJsonBody(true), + content = Data.formatJsonBodyWithBoolean(true), status = HttpStatusCode.OK ) } - val client = ConfigCatClient("test") { + val client = ConfigCatClient(Data.SDK_KEY) { httpEngine = mockEngine pollingMode = manualPoll() } client.forceRefresh() - assertEquals( - "ConfigCat-Kotlin/m-${Constants.version}", - mockEngine.requestHistory.last().headers["X-ConfigCat-UserAgent"] - ) + // For Js we check the query params + if (PlatformUtils.IS_BROWSER || PlatformUtils.IS_NODE) { + assertEquals( + "ConfigCat-Kotlin/m-${Constants.version}", + mockEngine.requestHistory.last().url.parameters["sdk"] + ) + } else { + assertEquals( + "ConfigCat-Kotlin/m-${Constants.version}", + mockEngine.requestHistory.last().headers["X-ConfigCat-UserAgent"] + ) + } } @Test @@ -562,7 +613,7 @@ class ConfigCatClientTests { status = HttpStatusCode.BadRequest ) } - val client = ConfigCatClient("test") { + val client = ConfigCatClient(Data.SDK_KEY) { httpEngine = mockEngine } @@ -570,18 +621,21 @@ class ConfigCatClientTests { assertEquals("", details.value) assertTrue(details.isDefaultValue) - assertEquals("Config JSON is not present when evaluating setting 'fakeKey'. Returning the `defaultValue` parameter that you specified in your application: ''.", details.error) + assertEquals( + "Config JSON is not present when evaluating setting 'fakeKey'. Returning the `defaultValue` parameter that you specified in your application: ''.", + details.error + ) } @Test fun testOnlineOffline() = runTest { val mockEngine = MockEngine { respond( - content = Data.formatJsonBody(true), + content = Data.formatJsonBodyWithBoolean(true), status = HttpStatusCode.OK ) } - val client = ConfigCatClient("test") { + val client = ConfigCatClient(Data.SDK_KEY) { httpEngine = mockEngine pollingMode = autoPoll { pollingInterval = 2.seconds } } @@ -611,11 +665,11 @@ class ConfigCatClientTests { fun testInitOffline() = runTest { val mockEngine = MockEngine { respond( - content = Data.formatJsonBody(true), + content = Data.formatJsonBodyWithBoolean(true), status = HttpStatusCode.OK ) } - val client = ConfigCatClient("test") { + val client = ConfigCatClient(Data.SDK_KEY) { httpEngine = mockEngine pollingMode = autoPoll { pollingInterval = 2.seconds } offline = true @@ -639,12 +693,12 @@ class ConfigCatClientTests { fun testInitOfflineCallsReady() = runTest { val mockEngine = MockEngine { respond( - content = Data.formatJsonBody(true), + content = Data.formatJsonBodyWithBoolean(true), status = HttpStatusCode.OK ) } var ready = false - ConfigCatClient("test") { + ConfigCatClient(Data.SDK_KEY) { httpEngine = mockEngine pollingMode = autoPoll { pollingInterval = 2.seconds } offline = true @@ -665,7 +719,7 @@ class ConfigCatClientTests { status = HttpStatusCode.OK ) } - val client = ConfigCatClient("test") { + val client = ConfigCatClient(Data.SDK_KEY) { httpEngine = mockEngine pollingMode = manualPoll() } @@ -696,7 +750,7 @@ class ConfigCatClientTests { status = HttpStatusCode.OK ) } - val client = ConfigCatClient("test") { + val client = ConfigCatClient(Data.SDK_KEY) { httpEngine = mockEngine pollingMode = manualPoll() } @@ -719,7 +773,7 @@ class ConfigCatClientTests { fun testHooks() = runTest { val mockEngine = MockEngine.create { this.addHandler { - respond(content = Data.formatJsonBody("test"), status = HttpStatusCode.OK) + respond(content = Data.formatJsonBodyWithString("test"), status = HttpStatusCode.OK) } this.addHandler { respond(content = "", status = HttpStatusCode.NotFound) @@ -729,7 +783,7 @@ class ConfigCatClientTests { var changed = false var ready = false - val client = ConfigCatClient("test") { + val client = ConfigCatClient(Data.SDK_KEY) { httpEngine = mockEngine pollingMode = manualPoll() hooks.addOnConfigChanged { changed = true } @@ -740,7 +794,10 @@ class ConfigCatClientTests { client.forceRefresh() client.forceRefresh() - assertEquals("Your SDK Key seems to be wrong. You can find the valid SDK Key at https://app.configcat.com/sdkkey. Received response: 404 Not Found", error) + assertEquals( + "Your SDK Key seems to be wrong. You can find the valid SDK Key at https://app.configcat.com/sdkkey. Received response: 404 Not Found", + error + ) assertTrue(changed) assertTrue(ready) } @@ -749,7 +806,7 @@ class ConfigCatClientTests { fun testHooksSub() = runTest { val mockEngine = MockEngine.create { this.addHandler { - respond(content = Data.formatJsonBody("test"), status = HttpStatusCode.OK) + respond(content = Data.formatJsonBodyWithString("test"), status = HttpStatusCode.OK) } this.addHandler { respond(content = "", status = HttpStatusCode.NotFound) @@ -758,7 +815,7 @@ class ConfigCatClientTests { var error = "" var changed = false - val client = ConfigCatClient("test") { + val client = ConfigCatClient(Data.SDK_KEY) { httpEngine = mockEngine pollingMode = manualPoll() } @@ -769,7 +826,10 @@ class ConfigCatClientTests { client.forceRefresh() client.forceRefresh() - assertEquals("Your SDK Key seems to be wrong. You can find the valid SDK Key at https://app.configcat.com/sdkkey. Received response: 404 Not Found", error) + assertEquals( + "Your SDK Key seems to be wrong. You can find the valid SDK Key at https://app.configcat.com/sdkkey. Received response: 404 Not Found", + error + ) assertTrue(changed) } @@ -777,14 +837,14 @@ class ConfigCatClientTests { fun testFail400() = runTest { val mockEngine = MockEngine.create { this.addHandler { - respond(content = Data.formatJsonBody("test"), status = HttpStatusCode.OK) + respond(content = Data.formatJsonBodyWithString("test"), status = HttpStatusCode.OK) } this.addHandler { respond(content = "", status = HttpStatusCode.BadRequest) } } - val client = ConfigCatClient("test") { + val client = ConfigCatClient(Data.SDK_KEY) { httpEngine = mockEngine pollingMode = manualPoll() } @@ -802,7 +862,7 @@ class ConfigCatClientTests { } var called = false - val client = ConfigCatClient("test") { + val client = ConfigCatClient(Data.SDK_KEY) { httpEngine = mockEngine pollingMode = manualPoll() hooks.addOnFlagEvaluated { details -> @@ -810,7 +870,10 @@ class ConfigCatClientTests { assertTrue(details.isDefaultValue) assertEquals("", details.value) assertEquals("ID", details.user?.identifier) - assertEquals("Config JSON is not present when evaluating setting 'fakeKey'. Returning the `defaultValue` parameter that you specified in your application: ''.", details.error) + assertEquals( + "Config JSON is not present when evaluating setting 'fakeKey'. Returning the `defaultValue` parameter that you specified in your application: ''.", + details.error + ) } } @@ -825,7 +888,7 @@ class ConfigCatClientTests { val mockEngine = MockEngine { respond(content = Data.formatConfigWithRules(), status = HttpStatusCode.OK) } - val client = ConfigCatClient("test") { + val client = ConfigCatClient(Data.SDK_KEY) { httpEngine = mockEngine pollingMode = manualPoll() } @@ -838,11 +901,14 @@ class ConfigCatClientTests { assertEquals("key", details.key) assertEquals("fakeId1", details.variationId) assertNull(details.error) - assertEquals("Identifier", details.matchedEvaluationRule?.comparisonAttribute) - assertEquals(2, details.matchedEvaluationRule?.comparator) - assertEquals("@test1.com", details.matchedEvaluationRule?.comparisonValue) - assertNull(details.matchedEvaluationPercentageRule) assertEquals("test@test1.com", details.user?.identifier) + assertEquals(1, details.matchedTargetingRule?.conditions?.size) + val condition = details.matchedTargetingRule?.conditions?.get(0) + + assertEquals("Identifier", condition?.userCondition?.comparisonAttribute) + assertEquals(2, condition?.userCondition?.comparator) + assertEquals("@test1.com", condition?.userCondition?.stringArrayValue?.get(0)) + assertNull(details.matchedPercentageOption) } @Test @@ -851,7 +917,7 @@ class ConfigCatClientTests { respond(content = Data.formatConfigWithRules(), status = HttpStatusCode.OK) } var called = false - val client = ConfigCatClient("test") { + val client = ConfigCatClient(Data.SDK_KEY) { httpEngine = mockEngine pollingMode = manualPoll() hooks.addOnFlagEvaluated { details -> @@ -860,11 +926,15 @@ class ConfigCatClientTests { assertEquals("key", details.key) assertEquals("fakeId1", details.variationId) assertNull(details.error) - assertEquals("Identifier", details.matchedEvaluationRule?.comparisonAttribute) - assertEquals(2, details.matchedEvaluationRule?.comparator) - assertEquals("@test1.com", details.matchedEvaluationRule?.comparisonValue) - assertNull(details.matchedEvaluationPercentageRule) assertEquals("test@test1.com", details.user?.identifier) + assertEquals(1, details.matchedTargetingRule?.conditions?.size) + val condition = details.matchedTargetingRule?.conditions?.get(0) + + assertEquals("Identifier", condition?.userCondition?.comparisonAttribute) + assertEquals(2, condition?.userCondition?.comparator) + assertEquals("@test1.com", condition?.userCondition?.stringArrayValue?.get(0)) + assertNull(details.matchedPercentageOption) + called = true } } @@ -876,45 +946,432 @@ class ConfigCatClientTests { @Test fun testSingleton() { - var client1 = ConfigCatClient("test") { flagOverrides = { behavior = OverrideBehavior.LOCAL_ONLY } } - val client2 = ConfigCatClient("test") { flagOverrides = { behavior = OverrideBehavior.LOCAL_ONLY } } + var client1 = ConfigCatClient(Data.SDK_KEY) { flagOverrides = { behavior = OverrideBehavior.LOCAL_ONLY } } + val client2 = ConfigCatClient(Data.SDK_KEY) { flagOverrides = { behavior = OverrideBehavior.LOCAL_ONLY } } assertSame(client1, client2) ConfigCatClient.closeAll() - client1 = ConfigCatClient("test") { flagOverrides = { behavior = OverrideBehavior.LOCAL_ONLY } } + client1 = ConfigCatClient(Data.SDK_KEY) { flagOverrides = { behavior = OverrideBehavior.LOCAL_ONLY } } assertNotSame(client1, client2) } @Test fun testRemoveTheClosingInstanceOnly() { - var client1 = ConfigCatClient("test") { flagOverrides = { behavior = OverrideBehavior.LOCAL_ONLY } } + val client1 = ConfigCatClient(Data.SDK_KEY) { flagOverrides = { behavior = OverrideBehavior.LOCAL_ONLY } } client1.close() - val client2 = ConfigCatClient("test") { flagOverrides = { behavior = OverrideBehavior.LOCAL_ONLY } } + val client2 = ConfigCatClient(Data.SDK_KEY) { flagOverrides = { behavior = OverrideBehavior.LOCAL_ONLY } } assertNotSame(client1, client2) client1.close() - val client3 = ConfigCatClient("test") { flagOverrides = { behavior = OverrideBehavior.LOCAL_ONLY } } + val client3 = ConfigCatClient(Data.SDK_KEY) { flagOverrides = { behavior = OverrideBehavior.LOCAL_ONLY } } assertSame(client2, client3) } @Test fun testClose() { - val client1 = ConfigCatClient("test") { flagOverrides = { behavior = OverrideBehavior.LOCAL_ONLY } } + val client1 = ConfigCatClient(Data.SDK_KEY) { flagOverrides = { behavior = OverrideBehavior.LOCAL_ONLY } } assertFalse(client1.isClosed()) client1.close() assertTrue(client1.isClosed()) } - companion object { - const val testMultipleBody = - """{ "f": { "key1": { "v": true, "i": "fakeId1" }, "key2": { "v": false, "i": "fakeId2" } } }""" + @Test + fun testSDKKeyIsNotEmpty() { + val exception = assertFailsWith(IllegalArgumentException::class, block = { + ConfigCatClient("") + }) + assertEquals("SDK Key cannot be empty.", exception.message) + } + + @Test + fun testSDKKeyIsValid() { + val mockEngine = MockEngine { + respond( + content = Data.formatJsonBodyWithBoolean(true), + status = HttpStatusCode.OK + ) + } + // TEST VALID KEYS + var client = ConfigCatClient("sdk-key-90123456789012/1234567890123456789012") { + httpEngine = mockEngine + } + assertNotNull(client) + client = ConfigCatClient("configcat-sdk-1/sdk-key-90123456789012/1234567890123456789012") { + httpEngine = mockEngine + } + assertNotNull(client) + client = ConfigCatClient("configcat-proxy/sdk-key-90123456789012") { + baseUrl = "https://my-configcat-proxy" + httpEngine = mockEngine + } + assertNotNull(client) + + ConfigCatClient.closeAll() + + // TEST INVALID KEYS + val wrongSDKKeys: List = listOf( + "sdk-key-90123456789012", + "sdk-key-9012345678901/1234567890123456789012", + "sdk-key-90123456789012/123456789012345678901", + "sdk-key-90123456789012/12345678901234567890123", + "sdk-key-901234567890123/1234567890123456789012", + "configcat-sdk-1/sdk-key-90123456789012", + "configcat-sdk-1/sdk-key-9012345678901/1234567890123456789012", + "configcat-sdk-1/sdk-key-90123456789012/123456789012345678901", + "configcat-sdk-1/sdk-key-90123456789012/12345678901234567890123", + "configcat-sdk-1/sdk-key-901234567890123/1234567890123456789012", + "configcat-sdk-2/sdk-key-90123456789012/1234567890123456789012", + "configcat-proxy/", + "configcat-proxy/sdk-key-90123456789012" + ) + wrongSDKKeys.forEach { + val exception = assertFailsWith(IllegalArgumentException::class, block = { + ConfigCatClient(it) + }) + assertEquals("SDK Key '$it' is invalid.", exception.message) + } + + val exception = assertFailsWith(IllegalArgumentException::class, block = { + ConfigCatClient("configcat-proxy/") { baseUrl = "https://my-configcat-proxy" } + }) + assertEquals("SDK Key 'configcat-proxy/' is invalid.", exception.message) + + // TEST OverrideBehaviour.localOnly skip sdkKey validation + client = + ConfigCatClient("sdk-key-90123456789012") { + flagOverrides = { behavior = OverrideBehavior.LOCAL_ONLY } + } + + assertNotNull(client) + + ConfigCatClient.closeAll() } + + @Test + fun testSpecialCharactersWorks() = runTest { + // override content with configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/u28_1qNyZ0Wz-ldYHIU7-g + val mockEngine = MockEngine { + respond( + content = specialCharacterContent, + status = HttpStatusCode.OK + ) + } + + val client = ConfigCatClient("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/u28_1qNyZ0Wz-ldYHIU7-g") { + httpEngine = mockEngine + } + // äöüÄÖÜçéèñışğ⢙✓😀 + val specialCharacters = "äöüÄÖÜçéèñışğ⢙✓\uD83D\uDE00" + + val user = ConfigCatUser(specialCharacters) + + assertEquals(specialCharacters, client.getValue("specialCharacters", "NOT_CAT", user)) + + assertEquals(specialCharacters, client.getValue("specialCharactersHashed", "NOT_CAT", user)) + + ConfigCatClient.closeAll() + } + + @Test + fun testGetValueValidTypes() = runTest { + val mockEngine = MockEngine { + respond( + content = testGetValueTypes, + status = HttpStatusCode.OK + ) + } + + val client = ConfigCatClient(Data.SDK_KEY) { + httpEngine = mockEngine + } + // String + assertEquals("fakeValueString", client.getValue("fakeKeyString", "default", null)) + // Boolean + assertEquals(true, client.getValue("fakeKeyBoolean", false, null)) + // Int + assertEquals(1, client.getValue("fakeKeyInt", 0, null)) + // Double + assertEquals(2.1, client.getValue("fakeKeyDouble", 1.1, null)) + + // getValue allows null. + val value1 = client.getValue("wrongKey", null, null) + assertNull(value1) + + // getAnyValue allows null. + val value2 = client.getAnyValue("wrongKey", null, null) + assertNull(value2) + + // getAnyValue allows any default value. + val defaultValue = ConfigCatUser("testId") + val value3 = client.getAnyValue("wrongKey", defaultValue, null) + assertEquals(defaultValue, value3) + } + + @Test + fun testGetValueInvalidTypes() = runTest { + val mockEngine = MockEngine { + respond( + content = testGetValueTypes, + status = HttpStatusCode.OK + ) + } + + val client = ConfigCatClient(Data.SDK_KEY) { + httpEngine = mockEngine + } + + // In case of JS the float is converted to an accepted type, in this case skip this test + if (!(PlatformUtils.IS_BROWSER || PlatformUtils.IS_NODE)) { + // Float + val floatException = assertFailsWith( + exceptionClass = IllegalArgumentException::class, + block = { client.getValue("fakeKeyString", 3.14f) } + ) + assertEquals("Only the following types are supported: String, Boolean, Int, Double (both nullable and non-nullable).", floatException.message) + } + + // Object + val exception = assertFailsWith( + exceptionClass = IllegalArgumentException::class, + block = { client.getValue("fakeKeyString", ConfigCatUser("testId")) } + ) + assertEquals("Only the following types are supported: String, Boolean, Int, Double (both nullable and non-nullable).", exception.message) + } + + @Test + fun testGetValueDetailsValidTypes() = runTest { + val mockEngine = MockEngine { + respond( + content = testGetValueTypes, + status = HttpStatusCode.OK + ) + } + + val client = ConfigCatClient(Data.SDK_KEY) { + httpEngine = mockEngine + } + // String + assertEquals("fakeValueString", client.getValueDetails("fakeKeyString", "default", null).value) + // Boolean + assertEquals(true, client.getValueDetails("fakeKeyBoolean", false, null).value) + // Int + assertEquals(1, client.getValueDetails("fakeKeyInt", 0, null).value) + // Double + assertEquals(2.1, client.getValueDetails("fakeKeyDouble", 1.1, null).value) + + // getValue allows null. + val value1 = client.getValueDetails("wrongKey", null, null).value + assertNull(value1) + + // getAnyValue allows null. + val value2 = client.getAnyValueDetails("wrongKey", null, null).value + assertNull(value2) + + // getAnyValue allows any default value. + val defaultValue = ConfigCatUser("testId") + val value3 = client.getAnyValueDetails("wrongKey", defaultValue, null).value + assertEquals(defaultValue, value3) + } + + @Test + fun testGetValueDetailsInvalidTypes() = runTest { + val mockEngine = MockEngine { + respond( + content = testGetValueTypes, + status = HttpStatusCode.OK + ) + } + + val client = ConfigCatClient(Data.SDK_KEY) { + httpEngine = mockEngine + } + + // In case of JS the float is converted to an accepted type, in this case skip this test + if (!(PlatformUtils.IS_BROWSER || PlatformUtils.IS_NODE)) { + // Float + val floatException = assertFailsWith( + exceptionClass = IllegalArgumentException::class, + block = { client.getValueDetails("fakeKeyString", 3.14f) } + ) + assertEquals("Only the following types are supported: String, Boolean, Int, Double (both nullable and non-nullable).", floatException.message) + } + + // Object + val exception = assertFailsWith( + exceptionClass = IllegalArgumentException::class, + block = { client.getValueDetails("fakeKeyString", ConfigCatUser("testId")) } + ) + assertEquals("Only the following types are supported: String, Boolean, Int, Double (both nullable and non-nullable).", exception.message) + } + + @Test + fun testFlagKeyAndVariationIdValidation() = runTest { + val mockEngine = MockEngine { + respond( + content = testGetValueTypes, + status = HttpStatusCode.OK + ) + } + + val client = ConfigCatClient(Data.SDK_KEY) { + httpEngine = mockEngine + } + + val exceptionGetValue = assertFailsWith( + exceptionClass = IllegalArgumentException::class, + block = { client.getValue("", "default", null) } + ) + assertEquals("'key' cannot be empty.", exceptionGetValue.message) + + val exceptionGetValueDetails = assertFailsWith( + exceptionClass = IllegalArgumentException::class, + block = { client.getValueDetails("", "default", null) } + ) + assertEquals("'key' cannot be empty.", exceptionGetValueDetails.message) + + val exceptionGetKeyAndValue = assertFailsWith( + exceptionClass = IllegalArgumentException::class, + block = { client.getKeyAndValue("") } + ) + assertEquals("'variationId' cannot be empty.", exceptionGetKeyAndValue.message) + } + + private val testGetValueTypes = """ + { + "p":{ + "s":"test-salt", + "u":"test" + }, + "f":{ + "fakeKeyString":{ + "t":1, + "v":{ + "s":"fakeValueString" + }, + "s":0, + "p":[ + + ], + "r":[ + + ] + }, + "fakeKeyInt":{ + "t":2, + "v":{ + "i":1 + }, + "s":0, + "p":[ + + ], + "r":[ + + ] + }, + "fakeKeyDouble":{ + "t":3, + "v":{ + "d":2.1 + }, + "s":0, + "p":[ + + ], + "r":[ + + ] + }, + "fakeKeyBoolean":{ + "t":0, + "v":{ + "b":true + }, + "s":0, + "p":[ + + ], + "r":[ + + ] + } + } + } + """.trimIndent() + + private val specialCharacterContent = """ + { + "p":{ + "u":"https://cdn-global.configcat.com", + "r":0, + "s":"ABWpFwDcdChe8DCLRnfe1qcRzFaRWqFKifbGCBnkHTU=" + }, + "f":{ + "specialCharacters":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Identifier", + "c":30, + "l":[ + "äöüÄÖÜçéèñışğ⢙✓😀" + ] + } + } + ], + "s":{ + "v":{ + "s":"äöüÄÖÜçéèñışğ⢙✓😀" + }, + "i":"1238ed4f" + } + } + ], + "v":{ + "s":"NOT_CAT" + }, + "i":"6a20318f" + }, + "specialCharactersHashed":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Identifier", + "c":22, + "l":[ + "40_4e37b40f1a89cf05c99a9451f6baa1d149a79c5f9a9a3793a6782c8eed9f605d" + ] + } + } + ], + "s":{ + "v":{ + "s":"äöüÄÖÜçéèñışğ⢙✓😀" + }, + "i":"bb95d969" + } + } + ], + "v":{ + "s":"NOT_CAT" + }, + "i":"33f810a1" + } + } + } + """ } diff --git a/src/commonTest/kotlin/com/configcat/ConfigFetcherTests.kt b/src/commonTest/kotlin/com/configcat/ConfigFetcherTests.kt index 69aeffc0..297386ce 100644 --- a/src/commonTest/kotlin/com/configcat/ConfigFetcherTests.kt +++ b/src/commonTest/kotlin/com/configcat/ConfigFetcherTests.kt @@ -2,6 +2,7 @@ package com.configcat import io.ktor.client.engine.mock.* import io.ktor.http.* +import io.ktor.util.* import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import kotlin.test.* @@ -22,7 +23,7 @@ class ConfigFetcherTests { val result = fetcher.fetch("") assertTrue(result.isFetched) - assertEquals("fakeValue", result.entry.config.settings["fakeKey"]?.value) + assertEquals("fakeValue", result.entry.config.settings?.get("fakeKey")?.settingValue?.stringValue) assertEquals(1, mockEngine.requestHistory.size) } @@ -76,10 +77,86 @@ class ConfigFetcherTests { assertTrue(resultNotModified.isNotModified) assertTrue(resultNotModified.entry.isEmpty()) assertEquals(2, mockEngine.requestHistory.size) + // For Js we run a separate test + if (PlatformUtils.IS_BROWSER || PlatformUtils.IS_NODE) { + assertEquals(eTag, mockEngine.requestHistory.last().url.parameters["ccetag"]) + } else { + assertEquals(eTag, mockEngine.requestHistory.last().headers["If-None-Match"]) + } + } + + @Test + fun testFetchParams() = runTest { + // For Js we run a separate test + if (PlatformUtils.IS_BROWSER || PlatformUtils.IS_NODE) { + return@runTest + } + val eTag = "test" + val mockEngine = MockEngine.create { + this.addHandler { + respond(content = testBody, status = HttpStatusCode.OK, headersOf(Pair("ETag", listOf(eTag)))) + } + this.addHandler { + respond(content = "", status = HttpStatusCode.NotModified) + } + } as MockEngine + val fetcher = Services.createFetcher(mockEngine) + fetcher.fetch("") + + assertEquals(1, mockEngine.requestHistory.size) + assertEquals( + "ConfigCat-Kotlin/a-${Constants.version}", + mockEngine.requestHistory.last().headers["X-ConfigCat-UserAgent"] + ) + assertEquals(null, mockEngine.requestHistory.last().headers["If-None-Match"]) + + fetcher.fetch(eTag) + + assertEquals(2, mockEngine.requestHistory.size) + assertEquals( + "ConfigCat-Kotlin/a-${Constants.version}", + mockEngine.requestHistory.last().headers["X-ConfigCat-UserAgent"] + ) + assertEquals(eTag, mockEngine.requestHistory.last().headers["If-None-Match"]) + } + + @Test + fun testFetchParamsWithHTTP2Headers() = runTest { + // For Js we run a separate test + if (PlatformUtils.IS_BROWSER || PlatformUtils.IS_NODE) { + return@runTest + } + val eTag = "test" + val mockEngine = MockEngine.create { + this.addHandler { + respond(content = testBody, status = HttpStatusCode.OK, headersOf(Pair("etag", listOf(eTag)))) + } + this.addHandler { + respond(content = "", status = HttpStatusCode.NotModified) + } + } as MockEngine + val fetcher = Services.createFetcher(mockEngine) + fetcher.fetch("") + + assertEquals(1, mockEngine.requestHistory.size) + assertEquals( + "ConfigCat-Kotlin/a-${Constants.version}", + mockEngine.requestHistory.last().headers["X-ConfigCat-UserAgent"] + ) + assertEquals(null, mockEngine.requestHistory.last().headers["If-None-Match"]) + + fetcher.fetch(eTag) + + assertEquals(2, mockEngine.requestHistory.size) + assertEquals( + "ConfigCat-Kotlin/a-${Constants.version}", + mockEngine.requestHistory.last().headers["X-ConfigCat-UserAgent"] + ) assertEquals(eTag, mockEngine.requestHistory.last().headers["If-None-Match"]) } companion object { - const val testBody = """{ "f": { "fakeKey": { "v": "fakeValue", "p": [], "r": [] } } }""" + const val testBody = + """{ "p": { "u": "https://cdn-global.configcat.com", "s": "test-slat" }, "f": { "fakeKey": { "t": 1, "v": {"s": "fakeValue" }, "p": [], "r": [], "a":""} }, "s": [] }""" } } diff --git a/src/commonTest/kotlin/com/configcat/ConfigServiceTests.kt b/src/commonTest/kotlin/com/configcat/ConfigServiceTests.kt index a45797ac..933cac08 100644 --- a/src/commonTest/kotlin/com/configcat/ConfigServiceTests.kt +++ b/src/commonTest/kotlin/com/configcat/ConfigServiceTests.kt @@ -20,20 +20,20 @@ class ConfigServiceTests { fun testAutoPollGet() = runTest { val mockEngine = MockEngine.create { this.addHandler { - respond(content = Data.formatJsonBody("test1"), status = HttpStatusCode.OK) + respond(content = Data.formatJsonBodyWithString("test1"), status = HttpStatusCode.OK) } this.addHandler { - respond(content = Data.formatJsonBody("test2"), status = HttpStatusCode.OK) + respond(content = Data.formatJsonBodyWithString("test2"), status = HttpStatusCode.OK) } } as MockEngine val service = Services.createConfigService(mockEngine, autoPoll { pollingInterval = 2.seconds }) val result = service.getSettings() - assertEquals("test1", result.settings["fakeKey"]?.value) + assertEquals("test1", result.settings.get("fakeKey")?.settingValue?.stringValue) TestUtils.awaitUntil { val result2 = service.getSettings() - result2.settings["fakeKey"]?.value == "test2" + result2.settings.get("fakeKey")?.settingValue?.stringValue == "test2" } assertTrue(mockEngine.requestHistory.size in 2..3) @@ -43,7 +43,7 @@ class ConfigServiceTests { fun testAutoPollGetFailed() = runTest { val mockEngine = MockEngine.create { this.addHandler { - respond(content = Data.formatJsonBody("test1"), status = HttpStatusCode.OK) + respond(content = Data.formatJsonBodyWithString("test1"), status = HttpStatusCode.OK) } this.addHandler { respond(content = "", status = HttpStatusCode.BadGateway) @@ -52,11 +52,11 @@ class ConfigServiceTests { val service = Services.createConfigService(mockEngine, autoPoll { pollingInterval = 2.seconds }) val result = service.getSettings() - assertEquals("test1", result.settings["fakeKey"]?.value) + assertEquals("test1", result.settings.get("fakeKey")?.settingValue?.stringValue) TestUtils.awaitUntil { val result2 = service.getSettings() - result2.settings["fakeKey"]?.value == "test1" && mockEngine.requestHistory.size in 2..3 + result2.settings.get("fakeKey")?.settingValue?.stringValue == "test1" && mockEngine.requestHistory.size in 2..3 } } @@ -64,10 +64,10 @@ class ConfigServiceTests { fun testAutoOnConfigChanged() = runTest { val mockEngine = MockEngine.create { this.addHandler { - respond(content = Data.formatJsonBody("test1"), status = HttpStatusCode.OK) + respond(content = Data.formatJsonBodyWithString("test1"), status = HttpStatusCode.OK) } this.addHandler { - respond(content = Data.formatJsonBody("test2"), status = HttpStatusCode.OK) + respond(content = Data.formatJsonBodyWithString("test2"), status = HttpStatusCode.OK) } } as MockEngine val service = Services.createConfigService( @@ -78,7 +78,7 @@ class ConfigServiceTests { ) val result = service.getSettings() - assertEquals("test1", result.settings["fakeKey"]?.value) + assertEquals("test1", result.settings.get("fakeKey")?.settingValue?.stringValue) assertTrue(mockEngine.requestHistory.size in 1..2) } @@ -87,20 +87,20 @@ class ConfigServiceTests { fun testLazyGet() = runTest { val mockEngine = MockEngine.create { this.addHandler { - respond(content = Data.formatJsonBody("test1"), status = HttpStatusCode.OK) + respond(content = Data.formatJsonBodyWithString("test1"), status = HttpStatusCode.OK) } this.addHandler { - respond(content = Data.formatJsonBody("test2"), status = HttpStatusCode.OK) + respond(content = Data.formatJsonBodyWithString("test2"), status = HttpStatusCode.OK) } } as MockEngine val service = Services.createConfigService(mockEngine, lazyLoad { cacheRefreshInterval = 2.seconds }) val result = service.getSettings() - assertEquals("test1", result.settings["fakeKey"]?.value) + assertEquals("test1", result.settings.get("fakeKey")?.settingValue?.stringValue) TestUtils.awaitUntil { val result2 = service.getSettings() - result2.settings["fakeKey"]?.value == "test2" + result2.settings.get("fakeKey")?.settingValue?.stringValue == "test2" } assertEquals(2, mockEngine.requestHistory.size) @@ -110,7 +110,7 @@ class ConfigServiceTests { fun testLazyGetFailed() = runTest { val mockEngine = MockEngine.create { this.addHandler { - respond(content = Data.formatJsonBody("test1"), status = HttpStatusCode.OK) + respond(content = Data.formatJsonBodyWithString("test1"), status = HttpStatusCode.OK) } this.addHandler { respond(content = "", status = HttpStatusCode.BadGateway) @@ -119,11 +119,11 @@ class ConfigServiceTests { val service = Services.createConfigService(mockEngine, lazyLoad { cacheRefreshInterval = 2.seconds }) val result = service.getSettings() - assertEquals("test1", result.settings["fakeKey"]?.value) + assertEquals("test1", result.settings.get("fakeKey")?.settingValue?.stringValue) TestUtils.awaitUntil { val result2 = service.getSettings() - result2.settings["fakeKey"]?.value == "test1" && mockEngine.requestHistory.size == 2 + result2.settings.get("fakeKey")?.settingValue?.stringValue == "test1" && mockEngine.requestHistory.size == 2 } } @@ -131,21 +131,21 @@ class ConfigServiceTests { fun testManualPollGet() = runTest { val mockEngine = MockEngine.create { this.addHandler { - respond(content = Data.formatJsonBody("test1"), status = HttpStatusCode.OK) + respond(content = Data.formatJsonBodyWithString("test1"), status = HttpStatusCode.OK) } this.addHandler { - respond(content = Data.formatJsonBody("test2"), status = HttpStatusCode.OK) + respond(content = Data.formatJsonBodyWithString("test2"), status = HttpStatusCode.OK) } } as MockEngine val service = Services.createConfigService(mockEngine, manualPoll()) service.refresh() val result = service.getSettings() - assertEquals("test1", result.settings["fakeKey"]?.value) + assertEquals("test1", result.settings.get("fakeKey")?.settingValue?.stringValue) service.refresh() val result2 = service.getSettings() - assertEquals("test2", result2.settings["fakeKey"]?.value) + assertEquals("test2", result2.settings.get("fakeKey")?.settingValue?.stringValue) assertEquals(2, mockEngine.requestHistory.size) } @@ -154,7 +154,7 @@ class ConfigServiceTests { fun testManualPollFailed() = runTest { val mockEngine = MockEngine.create { this.addHandler { - respond(content = Data.formatJsonBody("test1"), status = HttpStatusCode.OK) + respond(content = Data.formatJsonBodyWithString("test1"), status = HttpStatusCode.OK) } this.addHandler { respond(content = "", status = HttpStatusCode.BadGateway) @@ -164,11 +164,11 @@ class ConfigServiceTests { service.refresh() val result = service.getSettings() - assertEquals("test1", result.settings["fakeKey"]?.value) + assertEquals("test1", result.settings.get("fakeKey")?.settingValue?.stringValue) service.refresh() val result2 = service.getSettings() - assertEquals("test1", result2.settings["fakeKey"]?.value) + assertEquals("test1", result2.settings.get("fakeKey")?.settingValue?.stringValue) assertEquals(2, mockEngine.requestHistory.size) } @@ -177,7 +177,7 @@ class ConfigServiceTests { fun testAutoPollInitWaitTimeTimeout() = runTest { val mockEngine = MockEngine { delay(5000) - respond(content = Data.formatJsonBody("test1"), status = HttpStatusCode.OK) + respond(content = Data.formatJsonBodyWithString("test1"), status = HttpStatusCode.OK) } val start = DateTime.now() @@ -191,7 +191,7 @@ class ConfigServiceTests { val result = service.getSettings() val elapsed = DateTime.now() - start - assertNull(result.settings["fakeKey"]?.value) + assertNull(result.settings.get("fakeKey")?.settingValue?.stringValue) assertTrue(elapsed.seconds in 1.0..2.0) } @@ -199,16 +199,16 @@ class ConfigServiceTests { fun testNullCache() = runTest { val mockEngine = MockEngine.create { this.addHandler { - respond(content = Data.formatJsonBody("test1"), status = HttpStatusCode.OK) + respond(content = Data.formatJsonBodyWithString("test1"), status = HttpStatusCode.OK) } this.addHandler { - respond(content = Data.formatJsonBody("test2"), status = HttpStatusCode.OK) + respond(content = Data.formatJsonBodyWithString("test2"), status = HttpStatusCode.OK) } } as MockEngine val service = Services.createConfigService(mockEngine, autoPoll { pollingInterval = 1.seconds }, null) TestUtils.awaitUntil { - service.getSettings().settings.values.first().value == "test2" + service.getSettings().settings.values.first().settingValue.stringValue == "test2" } assertTrue(mockEngine.requestHistory.size in 2..3) @@ -218,7 +218,7 @@ class ConfigServiceTests { fun testAutoPollInitWaitTimeTimeoutReturnsWithCached() = runTest { val mockEngine = MockEngine { delay(5000) - respond(content = Data.formatJsonBody("test1"), status = HttpStatusCode.OK) + respond(content = Data.formatJsonBodyWithString("test1"), status = HttpStatusCode.OK) } val cache = SingleValueCache(Data.formatCacheEntryWithDate("test", Constants.distantPast)) val start = DateTime.now() @@ -232,7 +232,7 @@ class ConfigServiceTests { ) val result = service.getSettings() val elapsed = DateTime.now() - start - assertEquals("test", result.settings["fakeKey"]?.value) + assertEquals("test", result.settings.get("fakeKey")?.settingValue?.stringValue) println(elapsed) assertTrue(elapsed.seconds in 1.0..2.0) } @@ -241,10 +241,10 @@ class ConfigServiceTests { fun testAutoPollCacheWrite() = runTest { val mockEngine = MockEngine.create { this.addHandler { - respond(content = Data.formatJsonBody("test1"), status = HttpStatusCode.OK) + respond(content = Data.formatJsonBodyWithString("test1"), status = HttpStatusCode.OK) } this.addHandler { - respond(content = Data.formatJsonBody("test2"), status = HttpStatusCode.OK) + respond(content = Data.formatJsonBodyWithString("test2"), status = HttpStatusCode.OK) } } as MockEngine val cache = InMemoryCache() @@ -265,15 +265,15 @@ class ConfigServiceTests { fun testPollIntervalRespectsCacheExpiration() = runTest { val mockEngine = MockEngine.create { this.addHandler { - respond(content = Data.formatJsonBody("test"), status = HttpStatusCode.OK) + respond(content = Data.formatJsonBodyWithString("test"), status = HttpStatusCode.OK) } } as MockEngine val cache = SingleValueCache(Data.formatCacheEntry("test")) val service = Services.createConfigService(mockEngine, autoPoll { pollingInterval = 1.seconds }, cache) - val setting = service.getSettings().settings["fakeKey"] - assertEquals("test", setting?.value) + val setting = service.getSettings().settings.get("fakeKey") + assertEquals("test", setting?.settingValue?.stringValue) assertEquals(0, mockEngine.requestHistory.size) @@ -286,7 +286,7 @@ class ConfigServiceTests { fun testAutoPollOnlineOffline() = runTest { val mockEngine = MockEngine.create { this.addHandler { - respond(content = Data.formatJsonBody("test"), status = HttpStatusCode.OK) + respond(content = Data.formatJsonBodyWithString("test"), status = HttpStatusCode.OK) } } as MockEngine @@ -311,7 +311,7 @@ class ConfigServiceTests { fun testAutoPollInitOffline() = runTest { val mockEngine = MockEngine.create { this.addHandler { - respond(content = Data.formatJsonBody("test"), status = HttpStatusCode.OK) + respond(content = Data.formatJsonBodyWithString("test"), status = HttpStatusCode.OK) } } as MockEngine @@ -331,7 +331,7 @@ class ConfigServiceTests { fun testInitWaitTimeIgnoredWhenCacheIsNotExpired() = runTest { val mockEngine = MockEngine { delay(5000) - respond(content = Data.formatJsonBody("test"), status = HttpStatusCode.OK) + respond(content = Data.formatJsonBodyWithString("test"), status = HttpStatusCode.OK) } val start = DateTime.now() val cache = SingleValueCache(Data.formatCacheEntry("test")) @@ -345,7 +345,7 @@ class ConfigServiceTests { ) val result = service.getSettings() val elapsed = DateTime.now() - start - assertEquals("test", result.settings["fakeKey"]?.value) + assertEquals("test", result.settings.get("fakeKey")?.settingValue?.stringValue) assertTrue(elapsed.seconds < 1) } @@ -353,10 +353,10 @@ class ConfigServiceTests { fun testLazyCacheWrite() = runTest { val mockEngine = MockEngine.create { this.addHandler { - respond(content = Data.formatJsonBody("test1"), status = HttpStatusCode.OK) + respond(content = Data.formatJsonBodyWithString("test1"), status = HttpStatusCode.OK) } this.addHandler { - respond(content = Data.formatJsonBody("test2"), status = HttpStatusCode.OK) + respond(content = Data.formatJsonBodyWithString("test2"), status = HttpStatusCode.OK) } } as MockEngine val cache = InMemoryCache() @@ -376,7 +376,7 @@ class ConfigServiceTests { @Test fun testCacheExpirationRespectedInTTLCalc() = runTest { val mockEngine = MockEngine { - respond(content = Data.formatJsonBody("test"), status = HttpStatusCode.OK) + respond(content = Data.formatJsonBodyWithString("test"), status = HttpStatusCode.OK) } val cache = SingleValueCache(Data.formatCacheEntry("test")) val service = Services.createConfigService( @@ -426,9 +426,9 @@ class ConfigServiceTests { @Test fun testCacheTTLRespectsExternalCache() = runTest { val mockEngine = MockEngine { - respond(content = Data.formatJsonBody("test_remote"), status = HttpStatusCode.OK) + respond(content = Data.formatJsonBodyWithString("test_remote"), status = HttpStatusCode.OK) } - val cache = SingleValueCache(Data.formatCacheEntryWithEtag("test_local", "etag")) + val cache = SingleValueCache(Data.formatCacheEntryWithETag("test_local", "etag")) val service = Services.createConfigService( mockEngine, lazyLoad { @@ -436,12 +436,12 @@ class ConfigServiceTests { }, cache ) - assertEquals("test_local", service.getSettings().settings["fakeKey"]?.value.toString()) + assertEquals("test_local", service.getSettings().settings["fakeKey"]?.settingValue?.stringValue) assertEquals(0, mockEngine.requestHistory.size) TestUtils.wait(1.seconds) - cache.write("", Data.formatCacheEntryWithEtag("test_local2", "etag2")) - assertEquals("test_local2", service.getSettings().settings["fakeKey"]?.value.toString()) + cache.write("", Data.formatCacheEntryWithETag("test_local2", "etag2")) + assertEquals("test_local2", service.getSettings().settings["fakeKey"]?.settingValue?.stringValue) assertEquals(0, mockEngine.requestHistory.size) } @@ -449,7 +449,7 @@ class ConfigServiceTests { fun testLazyOnlineOffline() = runTest { val mockEngine = MockEngine.create { this.addHandler { - respond(content = Data.formatJsonBody("test"), status = HttpStatusCode.OK) + respond(content = Data.formatJsonBodyWithString("test"), status = HttpStatusCode.OK) } } as MockEngine @@ -474,11 +474,12 @@ class ConfigServiceTests { fun testLazyInitOffline() = runTest { val mockEngine = MockEngine.create { this.addHandler { - respond(content = Data.formatJsonBody("test"), status = HttpStatusCode.OK) + respond(content = Data.formatJsonBodyWithString("test"), status = HttpStatusCode.OK) } } as MockEngine - val service = Services.createConfigService(mockEngine, lazyLoad { cacheRefreshInterval = 1.seconds }, offline = true) + val service = + Services.createConfigService(mockEngine, lazyLoad { cacheRefreshInterval = 1.seconds }, offline = true) service.getSettings() @@ -498,10 +499,10 @@ class ConfigServiceTests { fun testManualCacheWrite() = runTest { val mockEngine = MockEngine.create { this.addHandler { - respond(content = Data.formatJsonBody("test1"), status = HttpStatusCode.OK) + respond(content = Data.formatJsonBodyWithString("test1"), status = HttpStatusCode.OK) } this.addHandler { - respond(content = Data.formatJsonBody("test2"), status = HttpStatusCode.OK) + respond(content = Data.formatJsonBodyWithString("test2"), status = HttpStatusCode.OK) } } as MockEngine val cache = InMemoryCache() @@ -520,7 +521,7 @@ class ConfigServiceTests { fun testManualOnlineOffline() = runTest { val mockEngine = MockEngine.create { this.addHandler { - respond(content = Data.formatJsonBody("test"), status = HttpStatusCode.OK) + respond(content = Data.formatJsonBodyWithString("test"), status = HttpStatusCode.OK) } } as MockEngine @@ -545,7 +546,7 @@ class ConfigServiceTests { fun testManualInitOffline() = runTest { val mockEngine = MockEngine.create { this.addHandler { - respond(content = Data.formatJsonBody("test"), status = HttpStatusCode.OK) + respond(content = Data.formatJsonBodyWithString("test"), status = HttpStatusCode.OK) } } as MockEngine @@ -568,12 +569,12 @@ class ConfigServiceTests { @Test fun testEnsureManualNotInitiatesHTTP() = runTest { val mockEngine = MockEngine { - respond(content = Data.formatJsonBody("test1"), status = HttpStatusCode.OK) + respond(content = Data.formatJsonBodyWithString("test1"), status = HttpStatusCode.OK) } val service = Services.createConfigService(mockEngine, manualPoll()) val result = service.getSettings() - assertNull(result.settings["fakeKey"]?.value) + assertNull(result.settings.get("fakeKey")?.settingValue?.stringValue) assertEquals(0, mockEngine.requestHistory.size) } @@ -581,17 +582,17 @@ class ConfigServiceTests { @Test fun testCacheKey() { val mockEngine = MockEngine { - respond(content = Data.formatJsonBody("test1"), status = HttpStatusCode.OK) + respond(content = Data.formatJsonBodyWithString("test1"), status = HttpStatusCode.OK) } val configCatOptions = ConfigCatOptions() - // Test Data: SDKKey "test1", HASH "147c5b4c2b2d7c77e1605b1a4309f0ea6684a0c6" - configCatOptions.sdkKey = "test1" + // Test Data: SDKKey "configcat-sdk-1/TEST_KEY-0123456789012/1234567890123456789012", HASH "f83ba5d45bceb4bb704410f51b704fb6dfa19942" + configCatOptions.sdkKey = "configcat-sdk-1/TEST_KEY-0123456789012/1234567890123456789012" val service = Services.createConfigService(mockEngine, options = configCatOptions) - assertEquals("147c5b4c2b2d7c77e1605b1a4309f0ea6684a0c6", service.cacheKey) + assertEquals("f83ba5d45bceb4bb704410f51b704fb6dfa19942", service.cacheKey) - // Test Data: SDKKey "test2", HASH "c09513b1756de9e4bc48815ec7a142b2441ed4d5" - configCatOptions.sdkKey = "test2" + // Test Data: SDKKey "configcat-sdk-1/TEST_KEY2-123456789012/1234567890123456789012", HASH "da7bfd8662209c8ed3f9db96daed4f8d91ba5876" + configCatOptions.sdkKey = "configcat-sdk-1/TEST_KEY2-123456789012/1234567890123456789012" val service2 = Services.createConfigService(mockEngine, options = configCatOptions) - assertEquals("c09513b1756de9e4bc48815ec7a142b2441ed4d5", service2.cacheKey) + assertEquals("da7bfd8662209c8ed3f9db96daed4f8d91ba5876", service2.cacheKey) } } diff --git a/src/commonTest/kotlin/com/configcat/ConfigV2EvaluationTest.kt b/src/commonTest/kotlin/com/configcat/ConfigV2EvaluationTest.kt new file mode 100644 index 00000000..6523f893 --- /dev/null +++ b/src/commonTest/kotlin/com/configcat/ConfigV2EvaluationTest.kt @@ -0,0 +1,2183 @@ +package com.configcat + +import com.configcat.evaluation.EvaluationTestLogger +import com.configcat.evaluation.LogEvent +import com.configcat.log.LogLevel +import com.configcat.override.OverrideBehavior +import com.configcat.override.OverrideDataSource +import com.soywiz.klock.DateTime +import io.ktor.client.engine.mock.* +import io.ktor.http.* +import io.ktor.util.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertContains +import kotlin.test.assertEquals + +@OptIn(ExperimentalCoroutinesApi::class) +class ConfigV2EvaluationTest { + @Test + fun circularDependencyTest() = runTest { + runCircularDependencyTest("key1", "'key1' -> 'key1'") + runCircularDependencyTest("key2", "'key2' -> 'key3' -> 'key2'") + runCircularDependencyTest("key4", "'key4' -> 'key3' -> 'key2' -> 'key3'") + } + + @Test + fun ruleAndPercentageOptionTest() = runTest { + runRuleAndPercentageOptionTest( + "12345", + null, + null, + "Cat", + expectedTargetingRule = false, + expectedPercentageOption = false + ) + runRuleAndPercentageOptionTest( + "12345", + "a@example.com", + null, + "Dog", + expectedTargetingRule = true, + expectedPercentageOption = false + ) + runRuleAndPercentageOptionTest( + "12345", + "a@configcat.com", + null, + "Cat", + expectedTargetingRule = false, + expectedPercentageOption = false + ) + runRuleAndPercentageOptionTest( + "12345", + "a@configcat.com", + "", + "Frog", + expectedTargetingRule = true, + expectedPercentageOption = true + ) + runRuleAndPercentageOptionTest( + "12345", + "a@configcat.com", + "US", + "Fish", + expectedTargetingRule = true, + expectedPercentageOption = true + ) + runRuleAndPercentageOptionTest( + "12345", + "b@configcat.com", + null, + "Cat", + expectedTargetingRule = false, + expectedPercentageOption = false + ) + runRuleAndPercentageOptionTest( + "12345", + "b@configcat.com", + "", + "Falcon", + expectedTargetingRule = false, + expectedPercentageOption = true + ) + runRuleAndPercentageOptionTest( + "12345", + "b@configcat.com", + "US", + "Spider", + expectedTargetingRule = false, + expectedPercentageOption = true + ) + } + + @Test + fun prerequisiteFlagTypeMismatchTest() = runTest { + if (PlatformUtils.IS_BROWSER || PlatformUtils.IS_NODE) { + return@runTest + } + runPrerequisiteFlagTypeMismatchTest("stringDependsOnBool", "mainBoolFlag", true, "Dog") + runPrerequisiteFlagTypeMismatchTest("stringDependsOnBool", "mainBoolFlag", false, "Cat") + runPrerequisiteFlagTypeMismatchTest("stringDependsOnBool", "mainBoolFlag", "1", "") + runPrerequisiteFlagTypeMismatchTest("stringDependsOnBool", "mainBoolFlag", 1, "") + runPrerequisiteFlagTypeMismatchTest("stringDependsOnBool", "mainBoolFlag", 1.0, "") + runPrerequisiteFlagTypeMismatchTest("stringDependsOnString", "mainStringFlag", "private", "Dog") + runPrerequisiteFlagTypeMismatchTest("stringDependsOnString", "mainStringFlag", "Private", "Cat") + runPrerequisiteFlagTypeMismatchTest("stringDependsOnString", "mainStringFlag", true, "") + runPrerequisiteFlagTypeMismatchTest("stringDependsOnString", "mainStringFlag", 1, "") + runPrerequisiteFlagTypeMismatchTest("stringDependsOnString", "mainStringFlag", 1.0, "") + runPrerequisiteFlagTypeMismatchTest("stringDependsOnInt", "mainIntFlag", 2, "Dog") + runPrerequisiteFlagTypeMismatchTest("stringDependsOnInt", "mainIntFlag", 1, "Cat") + runPrerequisiteFlagTypeMismatchTest("stringDependsOnInt", "mainIntFlag", "2", "") + runPrerequisiteFlagTypeMismatchTest("stringDependsOnInt", "mainIntFlag", true, "") + runPrerequisiteFlagTypeMismatchTest("stringDependsOnInt", "mainIntFlag", 2.0, "") + runPrerequisiteFlagTypeMismatchTest("stringDependsOnDouble", "mainDoubleFlag", 0.1, "Dog") + runPrerequisiteFlagTypeMismatchTest("stringDependsOnDouble", "mainDoubleFlag", 0.11, "Cat") + runPrerequisiteFlagTypeMismatchTest("stringDependsOnDouble", "mainDoubleFlag", "0.1", "") + runPrerequisiteFlagTypeMismatchTest("stringDependsOnDouble", "mainDoubleFlag", true, "") + runPrerequisiteFlagTypeMismatchTest("stringDependsOnDouble", "mainDoubleFlag", 1, "") + } + + @Test + fun prerequisiteFlagOverrideTest() = runTest { + runPrerequisiteFlagOverrideTest("stringDependsOnString", "1", "john@sensitivecompany.com", null, "Dog") + + runPrerequisiteFlagOverrideTest( + "stringDependsOnString", + "1", + "john@sensitivecompany.com", + OverrideBehavior.REMOTE_OVER_LOCAL, + "Dog" + ) + runPrerequisiteFlagOverrideTest( + "stringDependsOnString", + "1", + "john@sensitivecompany.com", + OverrideBehavior.LOCAL_OVER_REMOTE, + "Dog" + ) + runPrerequisiteFlagOverrideTest( + "stringDependsOnString", + "1", + "john@sensitivecompany.com", + OverrideBehavior.LOCAL_ONLY, + "" + ) + runPrerequisiteFlagOverrideTest("stringDependsOnString", "2", "john@notsensitivecompany.com", null, "Cat") + runPrerequisiteFlagOverrideTest( + "stringDependsOnString", + "2", + "john@notsensitivecompany.com", + OverrideBehavior.REMOTE_OVER_LOCAL, + "Cat" + ) + runPrerequisiteFlagOverrideTest( + "stringDependsOnString", + "2", + "john@notsensitivecompany.com", + OverrideBehavior.LOCAL_OVER_REMOTE, + "Dog" + ) + runPrerequisiteFlagOverrideTest( + "stringDependsOnString", + "2", + "john@notsensitivecompany.com", + OverrideBehavior.LOCAL_ONLY, + "" + ) + runPrerequisiteFlagOverrideTest( + "stringDependsOnInt", + "1", + "john@sensitivecompany.com", + null, + "Dog" + ) + runPrerequisiteFlagOverrideTest( + "stringDependsOnInt", + "1", + "john@sensitivecompany.com", + OverrideBehavior.REMOTE_OVER_LOCAL, + "Dog" + ) + runPrerequisiteFlagOverrideTest( + "stringDependsOnInt", + "1", + "john@sensitivecompany.com", + OverrideBehavior.LOCAL_OVER_REMOTE, + "Falcon" + ) + runPrerequisiteFlagOverrideTest( + "stringDependsOnInt", + "1", + "john@sensitivecompany.com", + OverrideBehavior.LOCAL_ONLY, + "Falcon" + ) + runPrerequisiteFlagOverrideTest( + "stringDependsOnInt", + "1", + "john@notsensitivecompany.com", + null, + "Cat" + ) + runPrerequisiteFlagOverrideTest( + "stringDependsOnInt", + "1", + "john@notsensitivecompany.com", + OverrideBehavior.REMOTE_OVER_LOCAL, + "Cat" + ) + runPrerequisiteFlagOverrideTest( + "stringDependsOnInt", + "1", + "john@notsensitivecompany.com", + OverrideBehavior.LOCAL_OVER_REMOTE, + "Falcon" + ) + runPrerequisiteFlagOverrideTest( + "stringDependsOnInt", + "1", + "john@notsensitivecompany.com", + OverrideBehavior.LOCAL_ONLY, + "Falcon" + ) + } + + @Test + fun runComparisonAttributeConversionToCanonicalStringRepresentationTest() = runTest { + runComparisonAttributeConversionToCanonicalStringRepresentationTest("numberToStringConversion", .12345, "1") + runComparisonAttributeConversionToCanonicalStringRepresentationTest( + "numberToStringConversionInt", + 125.toByte(), + "4" + ) + runComparisonAttributeConversionToCanonicalStringRepresentationTest( + "numberToStringConversionInt", + 125.toShort(), + "4" + ) + runComparisonAttributeConversionToCanonicalStringRepresentationTest("numberToStringConversionInt", 125, "4") + runComparisonAttributeConversionToCanonicalStringRepresentationTest("numberToStringConversionInt", 125L, "4") + runComparisonAttributeConversionToCanonicalStringRepresentationTest( + "numberToStringConversionPositiveExp", + -1.23456789e96, + "2" + ) + runComparisonAttributeConversionToCanonicalStringRepresentationTest( + "numberToStringConversionNegativeExp", + -12345.6789E-100, + "4" + ) + runComparisonAttributeConversionToCanonicalStringRepresentationTest( + "numberToStringConversionNaN", + Double.NaN, + "3" + ) + runComparisonAttributeConversionToCanonicalStringRepresentationTest( + "numberToStringConversionPositiveInf", + Double.POSITIVE_INFINITY, + "4" + ) + runComparisonAttributeConversionToCanonicalStringRepresentationTest( + "numberToStringConversionNegativeInf", + Double.NEGATIVE_INFINITY, + "3" + ) + runComparisonAttributeConversionToCanonicalStringRepresentationTest( + "numberToStringConversionPositiveExp", + -1.23456789e96, + "2" + ) + runComparisonAttributeConversionToCanonicalStringRepresentationTest( + "numberToStringConversionNegativeExp", + -12345.6789E-100, + "4" + ) + runComparisonAttributeConversionToCanonicalStringRepresentationTest( + "numberToStringConversionNaN", + Float.NaN, + "3" + ) + runComparisonAttributeConversionToCanonicalStringRepresentationTest( + "numberToStringConversionPositiveInf", + Float.POSITIVE_INFINITY, + "4" + ) + runComparisonAttributeConversionToCanonicalStringRepresentationTest( + "numberToStringConversionNegativeInf", + Float.NEGATIVE_INFINITY, + "3" + ) + if (!PlatformUtils.IS_NATIVE) { + // Native number format converts the double value to scientific notation causes a fail in these test cases + runComparisonAttributeConversionToCanonicalStringRepresentationTest( + "dateToStringConversion", + "date:2023-03-31T23:59:59.999Z", + "3" + ) + runComparisonAttributeConversionToCanonicalStringRepresentationTest( + "dateToStringConversion", + 1680307199.999, + "3" + ) + } + runComparisonAttributeConversionToCanonicalStringRepresentationTest( + "dateToStringConversionNaN", + Double.NaN, + "3" + ) + runComparisonAttributeConversionToCanonicalStringRepresentationTest( + "dateToStringConversionPositiveInf", + Double.POSITIVE_INFINITY, + "1" + ) + runComparisonAttributeConversionToCanonicalStringRepresentationTest( + "dateToStringConversionNegativeInf", + Double.NEGATIVE_INFINITY, + "5" + ) + runComparisonAttributeConversionToCanonicalStringRepresentationTest( + "stringArrayToStringConversion", + arrayOf("read", "Write", " eXecute "), + "4" + ) + runComparisonAttributeConversionToCanonicalStringRepresentationTest( + "stringArrayToStringConversionEmpty", + arrayOfNulls(0), + "5" + ) + runComparisonAttributeConversionToCanonicalStringRepresentationTest( + "stringArrayToStringConversionSpecialChars", + arrayOf("+<>%\"'\\/\t\r\n"), + "3" + ) + runComparisonAttributeConversionToCanonicalStringRepresentationTest( + "stringArrayToStringConversionUnicode", + arrayOf("äöüÄÖÜçéèñışğ⢙✓\uD83D\uDE00"), + "2" + ) + } + + private suspend fun runRuleAndPercentageOptionTest( + userId: String, + email: String?, + percentageBaseCustom: String?, + expectedValue: String?, + expectedTargetingRule: Boolean?, + expectedPercentageOption: Boolean? + ) { + val mockEngine = MockEngine { + respond(content = ruleOrOptionRemoteJson, status = HttpStatusCode.OK) + } + val client = ConfigCatClient("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw") { + pollingMode = manualPoll() + httpEngine = mockEngine + logLevel = LogLevel.ERROR + } + client.forceRefresh() + + val customAttributes = mutableMapOf() + if (percentageBaseCustom != null) { + customAttributes["PercentageBase"] = percentageBaseCustom + } + val configCatUser = ConfigCatUser(userId, email, null, customAttributes) + + val result = client.getValueDetails("stringMatchedTargetingRuleAndOrPercentageOption", "", configCatUser) + + assertEquals(expectedValue, result.value) + assertEquals(expectedTargetingRule, result.matchedTargetingRule != null) + assertEquals(expectedPercentageOption, result.matchedPercentageOption != null) + + ConfigCatClient.closeAll() + } + + private suspend fun runCircularDependencyTest(key: String, dependencyCycle: String) { + val mockEngine = MockEngine { + respond(content = circularDependencyTestRemoteJson, status = HttpStatusCode.OK) + } + val client = ConfigCatClient(Data.SDK_KEY) { + pollingMode = manualPoll() + httpEngine = mockEngine + logLevel = LogLevel.ERROR + } + client.forceRefresh() + + val valueDetails = client.getValueDetails(key, "", null) + assertEquals( + "Circular dependency detected between the following depending flags: $dependencyCycle.", + valueDetails.error + ) + + ConfigCatClient.closeAll() + } + + private suspend fun runPrerequisiteFlagTypeMismatchTest( + key: String, + prerequisiteFlagKey: String, + prerequisiteFlagValue: Any, + expectedValue: String? + ) { + val evaluationTestLogger = EvaluationTestLogger() + + val mockEngine = MockEngine { + respond(content = prerequisiteFlagMismatchRemoteJson, status = HttpStatusCode.OK) + } + val flagOverrideMap = mutableMapOf() + flagOverrideMap[prerequisiteFlagKey] = prerequisiteFlagValue + + val client = ConfigCatClient("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/JoGwdqJZQ0K2xDy7LnbyOg") { + pollingMode = manualPoll() + configCache = SingleValueCache("") + httpEngine = mockEngine + logLevel = LogLevel.ERROR + logger = evaluationTestLogger + flagOverrides = { + behavior = OverrideBehavior.LOCAL_OVER_REMOTE + dataSource = OverrideDataSource.map( + flagOverrideMap + ) + } + } + client.forceRefresh() + + val value = client.getValue(key, "", null) + val errorLogs = mutableListOf() + assertEquals( + expectedValue, + value, + "Flag key: $key PrerequisiteFlagKey: $prerequisiteFlagKey PrerequisiteFlagValue: $prerequisiteFlagValue" + ) + if (expectedValue.isNullOrEmpty()) { + val logsList = evaluationTestLogger.getLogList() + for (i in logsList.indices) { + val log = logsList[i] + if (log.logLevel == LogLevel.ERROR) { + errorLogs.add(log) + } + } + assertEquals(1, errorLogs.size, "Error size not matching") + val errorMessage: String = errorLogs[0].logMessage + assertContains(errorMessage, "[1002]") + + assertContains(errorMessage, "Type mismatch between comparison value") + + evaluationTestLogger.resetLogList() + } + + ConfigCatClient.closeAll() + } + + private suspend fun runPrerequisiteFlagOverrideTest( + key: String, + userId: String?, + email: String?, + overrideBehaviour: OverrideBehavior?, + expectedValue: Any? + ) { + var user: ConfigCatUser? = null + if (userId != null) { + user = ConfigCatUser(identifier = userId, email = email) + } + val overrideMap = mutableMapOf() + overrideMap["mainStringFlag"] = "private" + overrideMap["stringDependsOnInt"] = "Falcon" + + val mockEngine = MockEngine { + respond(content = prerequisiteFlagMismatchRemoteJson, status = HttpStatusCode.OK) + } + + val client = ConfigCatClient("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/JoGwdqJZQ0K2xDy7LnbyOg") { + pollingMode = manualPoll() + httpEngine = mockEngine + if (overrideBehaviour != null) { + flagOverrides = { + behavior = overrideBehaviour + dataSource = OverrideDataSource.map( + overrideMap + ) + } + } + } + client.forceRefresh() + + val value = client.getValue(key, "", user) + + assertEquals(expectedValue, value) + + ConfigCatClient.closeAll() + } + + private suspend fun runComparisonAttributeConversionToCanonicalStringRepresentationTest( + key: String, + userAttribute: Any, + expectedValue: String + ) { + val mockEngine = MockEngine { + respond( + content = comparisionAttributeConversionRemoteJson, + status = HttpStatusCode.OK + ) + } + val client = ConfigCatClient(Data.SDK_KEY) { + httpEngine = mockEngine + } + val userAttributeToMap: Any = if (userAttribute is String && userAttribute.startsWith("date:")) { + DateTime.fromString(userAttribute.substring(5)) + } else { + userAttribute + } + val customMap = mutableMapOf() + customMap["Custom1"] = userAttributeToMap + + val user = ConfigCatUser(identifier = "12345", custom = customMap) + + val result: String = client.getValue(key, "default", user) + + assertEquals(expectedValue, result) + + ConfigCatClient.closeAll() + } + + private val circularDependencyTestRemoteJson = """ + { + "p": { + "u": "https://cdn-global.configcat.com", + "r": 0, + "s": "test-salt" + }, + "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" } } + } + ] + } + } + } + """.trimIndent() + + private val ruleOrOptionRemoteJson = """ + { + "p":{ + "u":"https://cdn-global.configcat.com", + "r":0, + "s":"tpaRmJHutF5/zEKQVFTXvZ\u002BvFTT5BO28cJh9vbb\u002BNOE=" + }, + "f":{ + "dependentFlag":{ + "t":1, + "r":[ + { + "c":[ + { + "p":{ + "f":"key1", + "c":0, + "v":{ + "s":"value1" + } + } + } + ], + "s":{ + "v":{ + "s":"Chicken" + }, + "i":"5916066a" + } + }, + { + "c":[ + { + "p":{ + "f":"key1", + "c":0, + "v":{ + "s":"value1" + } + } + }, + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "12_6f86a4abfc7c40270d03abde842b48c426c1b03f1e59824df508ea2d4bba8eb8" + ] + } + } + ], + "s":{ + "v":{ + "s":"Cat" + }, + "i":"a4346a91" + } + } + ], + "v":{ + "s":"dependentFlag" + }, + "i":"e0afe4ca" + }, + "key1":{ + "t":1, + "v":{ + "s":"value1" + }, + "i":"1605ae93" + }, + "string75Cat0Dog25Falcon0HorseCustomAttr":{ + "t":1, + "a":"Country", + "p":[ + { + "p":75, + "v":{ + "s":"Cat" + }, + "i":"8285ed60" + }, + { + "p":0, + "v":{ + "s":"Dog" + }, + "i":"597e1dd1" + }, + { + "p":25, + "v":{ + "s":"Falcon" + }, + "i":"8896564a" + }, + { + "p":0, + "v":{ + "s":"Horse" + }, + "i":"d1944e2c" + } + ], + "v":{ + "s":"Chicken" + }, + "i":"13ad5bbc" + }, + "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat":{ + "t":1, + "a":"Country", + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@configcat.com" + ] + } + } + ], + "p":[ + { + "p":75, + "v":{ + "s":"Cat" + }, + "i":"05a1d8f3" + }, + { + "p":0, + "v":{ + "s":"Dog" + }, + "i":"52a42c84" + }, + { + "p":25, + "v":{ + "s":"Falcon" + }, + "i":"06c2db91" + }, + { + "p":0, + "v":{ + "s":"Horse" + }, + "i":"fe226091" + } + ] + } + ], + "v":{ + "s":"Cat" + }, + "i":"05a1d8f3" + }, + "stringMatchedTargetingRuleAndOrPercentageOption":{ + "t":1, + "a":"PercentageBase", + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":32, + "l":[ + "@example.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"7c01f064" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":30, + "l":[ + "a@" + ] + } + } + ], + "p":[ + { + "p":50, + "v":{ + "s":"Frog" + }, + "i":"8e2d8a91" + }, + { + "p":50, + "v":{ + "s":"Fish" + }, + "i":"7c67b71b" + } + ] + } + ], + "p":[ + { + "p":50, + "v":{ + "s":"Falcon" + }, + "i":"9e644055" + }, + { + "p":0, + "v":{ + "s":"Chicken" + }, + "i":"ceeb332c" + }, + { + "p":50, + "v":{ + "s":"Spider" + }, + "i":"fec43740" + } + ], + "v":{ + "s":"Cat" + }, + "i":"7d4140ce" + } + } + } + """.trimIndent() + + // Used for prerequisiteFlag Override as well + private val prerequisiteFlagMismatchRemoteJson = """ + { + "p":{ + "u":"https://cdn-global.configcat.com", + "r":0, + "s":"PBMv8zBDvXO9ZObbLwsP5TQOsgn8aOv1K3\u002BxPFJCoAU=" + }, + "f":{ + "boolDependsOnBool":{ + "t":0, + "r":[ + { + "c":[ + { + "p":{ + "f":"mainBoolFlag", + "c":0, + "v":{ + "b":true + } + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"8dc94c1d" + } + } + ], + "v":{ + "b":false + }, + "i":"d6194760" + }, + "boolDependsOnBoolDependsOnBool":{ + "t":0, + "r":[ + { + "c":[ + { + "p":{ + "f":"boolDependsOnBool", + "c":0, + "v":{ + "b":true + } + } + } + ], + "s":{ + "v":{ + "b":false + }, + "i":"d6870486" + } + } + ], + "v":{ + "b":true + }, + "i":"cd4c95e7" + }, + "boolDependsOnBoolInverse":{ + "t":0, + "r":[ + { + "c":[ + { + "p":{ + "f":"mainBoolFlagInverse", + "c":1, + "v":{ + "b":true + } + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"3c09bff0" + } + } + ], + "v":{ + "b":false + }, + "i":"cecbc501" + }, + "doubleDependsOnBool":{ + "t":3, + "r":[ + { + "c":[ + { + "p":{ + "f":"mainBoolFlag", + "c":0, + "v":{ + "b":true + } + } + } + ], + "s":{ + "v":{ + "d":1.1 + }, + "i":"271fd003" + } + } + ], + "v":{ + "d":3.14 + }, + "i":"718aae2b" + }, + "intDependsOnBool":{ + "t":2, + "r":[ + { + "c":[ + { + "p":{ + "f":"mainBoolFlag", + "c":0, + "v":{ + "b":true + } + } + } + ], + "s":{ + "v":{ + "i":1 + }, + "i":"d2dda649" + } + } + ], + "v":{ + "i":42 + }, + "i":"43ec49a8" + }, + "mainBoolFlag":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "21_32abe94b0866402b226383eb666a98312dc898119e2a9241ffbfcc114eb6a57b" + ] + } + } + ], + "s":{ + "v":{ + "b":false + }, + "i":"e842ea6f" + } + } + ], + "v":{ + "b":true + }, + "i":"8a68b064" + }, + "mainBoolFlagEmpty":{ + "t":0, + "v":{ + "b":true + }, + "i":"f3295d43" + }, + "mainBoolFlagInverse":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "21_69627ce988f31d14807ed75022d5325645914dadc3bfe7cdc1b6dbeca8763b67" + ] + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"28c65f1f" + } + } + ], + "v":{ + "b":false + }, + "i":"d70e47a7" + }, + "mainDoubleFlag":{ + "t":3, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "21_4cb521a31b1b604875ec3c7c90553a7cb692434f9aee8a318215f9bf1165f0e3" + ] + } + } + ], + "s":{ + "v":{ + "d":0.1 + }, + "i":"a67947ed" + } + } + ], + "v":{ + "d":3.14 + }, + "i":"beb3acc7" + }, + "mainIntFlag":{ + "t":2, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "21_0ad4d095ab7ae197936c7dde2a53e55b2df616c0845c9b216ade6f14b2a4cf3d" + ] + } + } + ], + "s":{ + "v":{ + "i":2 + }, + "i":"67e14078" + } + } + ], + "v":{ + "i":42 + }, + "i":"a7490aca" + }, + "mainStringFlag":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "21_78d8c5a677414bd170650ec60b51e9325663ef8447b280862ec52be49cca7b0f" + ] + } + } + ], + "s":{ + "v":{ + "s":"private" + }, + "i":"51b57fb0" + } + } + ], + "v":{ + "s":"public" + }, + "i":"24c96275" + }, + "stringDependsOnBool":{ + "t":1, + "r":[ + { + "c":[ + { + "p":{ + "f":"mainBoolFlag", + "c":0, + "v":{ + "b":true + } + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"fc8daf80" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"d53a2b42" + }, + "stringDependsOnDouble":{ + "t":1, + "r":[ + { + "c":[ + { + "p":{ + "f":"mainDoubleFlag", + "c":0, + "v":{ + "d":0.1 + } + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"84fc7ed9" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"9cc8fd8f" + }, + "stringDependsOnDoubleIntValue":{ + "t":1, + "r":[ + { + "c":[ + { + "p":{ + "f":"mainDoubleFlag", + "c":0, + "v":{ + "d":0 + } + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"842c1d75" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"db7f56c8" + }, + "stringDependsOnEmptyBool":{ + "t":1, + "r":[ + { + "c":[ + { + "p":{ + "f":"mainBoolFlagEmpty", + "c":0, + "v":{ + "b":true + } + } + } + ], + "s":{ + "v":{ + "s":"EmptyOn" + }, + "i":"d5508c78" + } + } + ], + "v":{ + "s":"EmptyOff" + }, + "i":"8e0dbe88" + }, + "stringDependsOnInt":{ + "t":1, + "r":[ + { + "c":[ + { + "p":{ + "f":"mainIntFlag", + "c":0, + "v":{ + "i":2 + } + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"12531eec" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"e227d926" + }, + "stringDependsOnString":{ + "t":1, + "r":[ + { + "c":[ + { + "p":{ + "f":"mainStringFlag", + "c":0, + "v":{ + "s":"private" + } + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"426b6d4d" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"d36000e1" + }, + "stringDependsOnStringCaseCheck":{ + "t":1, + "r":[ + { + "c":[ + { + "p":{ + "f":"mainStringFlag", + "c":0, + "v":{ + "s":"Private" + } + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"87d24aed" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"ad94f385" + }, + "stringInverseDependsOnEmptyBool":{ + "t":1, + "r":[ + { + "c":[ + { + "p":{ + "f":"mainBoolFlagEmpty", + "c":1, + "v":{ + "b":true + } + } + } + ], + "s":{ + "v":{ + "s":"EmptyOff" + }, + "i":"b7c3efae" + } + } + ], + "v":{ + "s":"EmptyOn" + }, + "i":"f6b4b8a2" + } + } + } + """.trimIndent() + + private val comparisionAttributeConversionRemoteJson = """ + { + "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" + }, + "i": "test-variation-id" + }, + "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" + }, + "i": "test-variation-id" + }, + "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" + }, + "i": "test-variation-id" + }, + "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" + }, + "i": "test-variation-id" + }, + "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" + }, + "i": "test-variation-id" + }, + "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" + }, + "i": "test-variation-id" + }, + "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" + }, + "i": "test-variation-id" + }, + "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" + }, + "i": "test-variation-id" + }, + "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" + }, + "i": "test-variation-id" + }, + "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" + }, + "i": "test-variation-id" + }, + "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" + }, + "i": "test-variation-id" + }, + "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" + }, + "i": "test-variation-id" + }, + "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" + }, + "i": "test-variation-id" + }, + "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" + }, + "i": "test-variation-id" + }, + "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" + }, + "i": "test-variation-id" + } + } +} + """ +} diff --git a/src/commonTest/kotlin/com/configcat/DataGovernanceTests.kt b/src/commonTest/kotlin/com/configcat/DataGovernanceTests.kt index f981a50c..1bf6cab6 100644 --- a/src/commonTest/kotlin/com/configcat/DataGovernanceTests.kt +++ b/src/commonTest/kotlin/com/configcat/DataGovernanceTests.kt @@ -160,7 +160,7 @@ class DataGovernanceTests { companion object { fun formatBody(url: String, redirect: Int): String { - return """{ "p": { "u": "$url", "r": $redirect }, "f": {} }""" + return """{ "p": { "u": "$url", "r": $redirect, "s": "test-salt" }, "f": {}, "s":[] }""" } const val customCdnUrl = "https://custom-cdn.configcat.com" diff --git a/src/commonTest/kotlin/com/configcat/EntrySerializationTests.kt b/src/commonTest/kotlin/com/configcat/EntrySerializationTests.kt index e9186c2e..116d52e5 100644 --- a/src/commonTest/kotlin/com/configcat/EntrySerializationTests.kt +++ b/src/commonTest/kotlin/com/configcat/EntrySerializationTests.kt @@ -1,5 +1,7 @@ package com.configcat +import com.configcat.model.Config +import com.configcat.model.Entry import com.soywiz.klock.DateTime import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest @@ -15,7 +17,7 @@ class EntrySerializationTests { @Test fun testSerialize() = runTest { - val json: String = Data.formatJsonBody("test") + val json: String = Data.formatJsonBodyWithString("test") val config: Config = Constants.json.decodeFromString(json) val fetchTimeNow = DateTime.now() val entry = Entry(config, "fakeTag", json, fetchTimeNow) @@ -29,7 +31,7 @@ class EntrySerializationTests { @Test fun testPayloadSerializationPlatformIndependent() { val payloadTestConfigJson = - "{\"p\":{\"u\":\"https://cdn-global.configcat.com\",\"r\":0},\"f\":{\"testKey\":{\"v\":\"testValue\",\"t\":1,\"p\":[],\"r\":[]}}}" + "{\"p\":{\"u\":\"https://cdn-global.configcat.com\",\"r\":0,\"s\":\"test-slat\"},\"f\":{\"testKey\":{\"v\":{\"s\":\"testValue\"},\"t\":1,\"p\":[],\"r\":[], \"a\":\"\"}}, \"s\":[] }" val config: Config = Constants.json.decodeFromString(payloadTestConfigJson) val entry = Entry(config, "test-etag", payloadTestConfigJson, DateTime(1686756435844L)) @@ -40,7 +42,7 @@ class EntrySerializationTests { @Test fun testDeserialize() = runTest { - val json: String = Data.formatJsonBody("test") + val json: String = Data.formatJsonBodyWithString("test") val dateTimeNow = DateTime.now() val dateTimeNowUnixSeconds: Long = dateTimeNow.unixMillis.toLong() @@ -51,7 +53,7 @@ class EntrySerializationTests { assertEquals(dateTimeNow, entry.fetchTime) assertEquals("fakeTag", entry.eTag) assertEquals(json, entry.configJson) - assertEquals(1, entry.config.settings.size) + assertEquals(1, entry.config.settings?.size) } @Test diff --git a/src/commonTest/kotlin/com/configcat/EvaluatorTrimTests.kt b/src/commonTest/kotlin/com/configcat/EvaluatorTrimTests.kt new file mode 100644 index 00000000..78c97d8d --- /dev/null +++ b/src/commonTest/kotlin/com/configcat/EvaluatorTrimTests.kt @@ -0,0 +1,1946 @@ +package com.configcat + +import io.ktor.client.engine.mock.* +import io.ktor.http.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals + +@OptIn(ExperimentalCoroutinesApi::class) +class EvaluatorTrimTests { + + private val testIdentifier = "12345" + private val testVersion = "1.0.0" + private val testNumber = "3" + private val testCountry = "[\"USA\"]" + private val testCountryWithWhiteSpaces = "[\" USA \"]" + + // 1705253400 - 2014.01.14 17:30:00 +1 - test check between 17:00 and 18:00 + private val testDate = "1705253400" + + @Test + fun runComparatorValueTrimsTest() = runTest { + runComparatorValueTrimsTest("isoneof", "no trim") + runComparatorValueTrimsTest("isnotoneof", "no trim") + runComparatorValueTrimsTest("containsanyof", "no trim") + runComparatorValueTrimsTest("notcontainsanyof", "no trim") + runComparatorValueTrimsTest("isoneofhashed", "no trim") + runComparatorValueTrimsTest("isnotoneofhashed", "no trim") + runComparatorValueTrimsTest("equalshashed", "no trim") + runComparatorValueTrimsTest("notequalshashed", "no trim") + runComparatorValueTrimsTest("arraycontainsanyofhashed", "no trim") + runComparatorValueTrimsTest("arraynotcontainsanyofhashed", "no trim") + runComparatorValueTrimsTest("equals", "no trim") + runComparatorValueTrimsTest("notequals", "no trim") + runComparatorValueTrimsTest("startwithanyof", "no trim") + runComparatorValueTrimsTest("notstartwithanyof", "no trim") + runComparatorValueTrimsTest("endswithanyof", "no trim") + runComparatorValueTrimsTest("notendswithanyof", "no trim") + runComparatorValueTrimsTest("arraycontainsanyof", "no trim") + runComparatorValueTrimsTest("arraynotcontainsanyof", "no trim") + // the not trimmed comparator value case an exception in case of these comparator, default value expected + runComparatorValueTrimsTest("startwithanyofhashed", "no trim") + runComparatorValueTrimsTest("notstartwithanyofhashed", "no trim") + runComparatorValueTrimsTest("endswithanyofhashed", "no trim") + runComparatorValueTrimsTest("notendswithanyofhashed", "no trim") + // semver comparator values trimmed because of backward compatibility + runComparatorValueTrimsTest("semverisoneof", "4 trim") + runComparatorValueTrimsTest("semverisnotoneof", "5 trim") + runComparatorValueTrimsTest("semverless", "6 trim") + runComparatorValueTrimsTest("semverlessequals", "7 trim") + runComparatorValueTrimsTest("semvergreater", "8 trim") + runComparatorValueTrimsTest("semvergreaterequals", "9 trim") + } + + private suspend fun runComparatorValueTrimsTest( + key: String, + expectedValue: String + ) { + val mockEngine = MockEngine { + respond( + content = comparatorTestContent, + status = HttpStatusCode.OK + ) + } + val client = ConfigCatClient(Data.SDK_KEY) { + httpEngine = mockEngine + } + val user: ConfigCatUser = createTestUser(testIdentifier, testCountry, testVersion, testNumber, testDate) + + val result: String = client.getValue(key, "default", user) + + assertEquals(expectedValue, result) + + ConfigCatClient.closeAll() + } + + @Test + fun runUserValueTrimsTest() = runTest { + runUserValueTrimsTest("isoneof", "no trim") + runUserValueTrimsTest("isnotoneof", "no trim") + runUserValueTrimsTest("isoneofhashed", "no trim") + runUserValueTrimsTest("isnotoneofhashed", "no trim") + runUserValueTrimsTest("equalshashed", "no trim") + runUserValueTrimsTest("notequalshashed", "no trim") + runUserValueTrimsTest("arraycontainsanyofhashed", "no trim") + runUserValueTrimsTest("arraynotcontainsanyofhashed", "no trim") + runUserValueTrimsTest("equals", "no trim") + runUserValueTrimsTest("notequals", "no trim") + runUserValueTrimsTest("startwithanyof", "no trim") + runUserValueTrimsTest("notstartwithanyof", "no trim") + runUserValueTrimsTest("endswithanyof", "no trim") + runUserValueTrimsTest("notendswithanyof", "no trim") + runUserValueTrimsTest("arraycontainsanyof", "no trim") + runUserValueTrimsTest("arraynotcontainsanyof", "no trim") + runUserValueTrimsTest("startwithanyofhashed", "no trim") + runUserValueTrimsTest("notstartwithanyofhashed", "no trim") + runUserValueTrimsTest("endswithanyofhashed", "no trim") + runUserValueTrimsTest("notendswithanyofhashed", "no trim") + // semver comparators user values trimmed because of backward compatibility + // semver comparators user values trimmed because of backward compatibility + runUserValueTrimsTest("semverisoneof", "4 trim") + runUserValueTrimsTest("semverisnotoneof", "5 trim") + runUserValueTrimsTest("semverless", "6 trim") + runUserValueTrimsTest("semverlessequals", "7 trim") + runUserValueTrimsTest("semvergreater", "8 trim") + runUserValueTrimsTest("semvergreaterequals", "9 trim") + // number and date comparators user values trimmed because of backward compatibility + // number and date comparators user values trimmed because of backward compatibility + runUserValueTrimsTest("numberequals", "10 trim") + runUserValueTrimsTest("numbernotequals", "11 trim") + runUserValueTrimsTest("numberless", "12 trim") + runUserValueTrimsTest("numberlessequals", "13 trim") + runUserValueTrimsTest("numbergreater", "14 trim") + runUserValueTrimsTest("numbergreaterequals", "15 trim") + runUserValueTrimsTest("datebefore", "18 trim") + runUserValueTrimsTest("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. + // "contains any of" and "not contains any of" is a special case, the not trimmed user attribute checked against not trimmed comparator values. + runUserValueTrimsTest("containsanyof", "no trim") + runUserValueTrimsTest("notcontainsanyof", "no trim") + } + + private suspend fun runUserValueTrimsTest( + key: String, + expectedValue: String + ) { + val mockEngine = MockEngine { + respond( + content = userTestContent, + status = HttpStatusCode.OK + ) + } + val client = ConfigCatClient(Data.SDK_KEY) { + httpEngine = mockEngine + } + val user: ConfigCatUser = createTestUser( + addWhiteSpaces(testIdentifier), + testCountryWithWhiteSpaces, + addWhiteSpaces(testVersion), + addWhiteSpaces(testNumber), + addWhiteSpaces(testDate) + ) + + val result: String = client.getValue(key, "default", user) + + assertEquals(expectedValue, result) + + ConfigCatClient.closeAll() + } + + private fun createTestUser( + id: String, + country: String, + version: String, + number: String, + date: String + ): ConfigCatUser { + val customMap = mutableMapOf() + customMap["Version"] = version + customMap["Number"] = number + customMap["Date"] = date + return ConfigCatUser(identifier = id, country = country, custom = customMap) + } + + private fun addWhiteSpaces(raw: String): String { + return " $raw " + } + + /** + * The comparatorTestContent contains settings with invalid comparator values. The server default handles the + * trimming. To test the client the comparator values contains pre and post whitespaces. + */ + private val comparatorTestContent = """ + { + "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" + } + } + } + """ + + /** + * trim_user_values.json contains valid settings. Expect "containsanyof" and "notcontainsanyof" flags where the + * comparator values contains pre and post white spaces, the untrimmed user value can be properly compared + * against the invalid data. + */ + private val userTestContent = """ + { + "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" + } + } + } + """ +} diff --git a/src/commonTest/kotlin/com/configcat/OverrideTests.kt b/src/commonTest/kotlin/com/configcat/OverrideTests.kt index 2456c3bd..7bd2fe09 100644 --- a/src/commonTest/kotlin/com/configcat/OverrideTests.kt +++ b/src/commonTest/kotlin/com/configcat/OverrideTests.kt @@ -1,5 +1,6 @@ package com.configcat +import com.configcat.model.* import com.configcat.override.OverrideBehavior import com.configcat.override.OverrideDataSource import io.ktor.client.engine.mock.* @@ -20,9 +21,9 @@ class OverrideTests { @Test fun testLocalOnly() = runTest { val mockEngine = MockEngine { - respond(content = Data.formatJsonBody(false), status = HttpStatusCode.OK) + respond(content = Data.formatJsonBodyWithBoolean(false), status = HttpStatusCode.OK) } - val client = ConfigCatClient("local") { + val client = ConfigCatClient(Data.SDK_KEY) { httpEngine = mockEngine flagOverrides = { behavior = OverrideBehavior.LOCAL_ONLY @@ -49,9 +50,9 @@ class OverrideTests { @Test fun testLocalOverRemote() = runTest { val mockEngine = MockEngine { - respond(content = Data.formatJsonBody(false), status = HttpStatusCode.OK) + respond(content = Data.formatJsonBodyWithBoolean(false), status = HttpStatusCode.OK) } - val client = ConfigCatClient("local") { + val client = ConfigCatClient(Data.SDK_KEY) { httpEngine = mockEngine flagOverrides = { behavior = OverrideBehavior.LOCAL_OVER_REMOTE @@ -72,9 +73,9 @@ class OverrideTests { @Test fun testRemoteOverLocal() = runTest { val mockEngine = MockEngine { - respond(content = Data.formatJsonBody(false), status = HttpStatusCode.OK) + respond(content = Data.formatJsonBodyWithBoolean(false), status = HttpStatusCode.OK) } - val client = ConfigCatClient("local") { + val client = ConfigCatClient(Data.SDK_KEY) { httpEngine = mockEngine flagOverrides = { behavior = OverrideBehavior.REMOTE_OVER_LOCAL @@ -95,35 +96,61 @@ class OverrideTests { @Test fun testSettingOverride() = runTest { val mockEngine = MockEngine { - respond(content = Data.formatJsonBody(false), status = HttpStatusCode.OK) + respond(content = Data.formatJsonBodyWithBoolean(false), status = HttpStatusCode.OK) } + val user = ConfigCatUser("test@test1.com") - val client = ConfigCatClient("local") { + val client = ConfigCatClient(Data.SDK_KEY) { httpEngine = mockEngine flagOverrides = { behavior = OverrideBehavior.LOCAL_ONLY - dataSource = OverrideDataSource.settings( - mapOf( - "noRuleOverride" to Setting("noRule", 1, emptyList(), emptyList(), "myVariationId"), - "ruleOverride" to Setting( - "noMatch", - 1, - emptyList(), - listOf( - RolloutRule("ruleMatch", "Identifier", 2, "@test1", "ruleVariationId") + dataSource = OverrideDataSource.config( + config = Config( + preferences = Preferences(baseUrl = "test", salt = "test-salt"), + settings = mapOf( + "noRuleOverride" to Setting( + 1, + "", + null, + null, + SettingValue(stringValue = "noRule"), + "myVariationId" ), - "myVariationId" - ), - "percentageOverride" to Setting( - "noMatch", - 1, - listOf( - PercentageRule("A", 75.0, "percentageAVariationID"), - PercentageRule("B", 25.0, "percentageAVariationID") + "ruleOverride" to Setting( + 1, + "", + null, + arrayOf( + TargetingRule( + conditions = arrayOf( + Condition( + UserCondition("Identifier", 2, stringArrayValue = arrayOf("@test1")), + null, + null + ) + ), + null, + ServedValue( + SettingValue(stringValue = "ruleMatch"), + "ruleVariationId" + ) + ) + ), + SettingValue(stringValue = "noMatch"), + "myVariationId" ), - emptyList(), - "myVariationId" + "percentageOverride" to Setting( + 1, + null, + arrayOf( + PercentageOption(75, SettingValue(stringValue = "A"), "percentageAVariationID"), + PercentageOption(25, SettingValue(stringValue = "B"), "percentageAVariationID") + ), + emptyArray(), + SettingValue(stringValue = "noMatch"), + "myVariationId" + ) ) ) ) diff --git a/src/commonTest/kotlin/com/configcat/Utils.kt b/src/commonTest/kotlin/com/configcat/Utils.kt index d608d29f..374d8519 100644 --- a/src/commonTest/kotlin/com/configcat/Utils.kt +++ b/src/commonTest/kotlin/com/configcat/Utils.kt @@ -2,6 +2,7 @@ package com.configcat import com.configcat.fetch.ConfigFetcher import com.configcat.log.InternalLogger +import com.configcat.model.* import com.soywiz.klock.DateTime import io.ktor.client.engine.mock.* import kotlinx.coroutines.Dispatchers @@ -34,52 +35,99 @@ internal object TestUtils { } internal object Data { - fun formatJsonBody(value: Any): String { - return """{ "f": { "fakeKey": { "v": $value, "p": [], "r": [] } } }""" + const val SDK_KEY = "configcat-sdk-1/TEST_KEY-0123456789012/1234567890123456789012" + + const val MULTIPLE_BODY = + """{ "p": {"u": "https://cdn-global.configcat.com", "s": "test-salt" }, "f": { "key1": { "t": 0, "v": { "b": true}, "i": "fakeId1", "p": [], "r": [], "a" : ""}, "key2": { "t": 0, "v": {"b": false}, "i": "fakeId2", "p": [], "r": [], "a":"" }}, "s": [] }""" + + fun formatJsonBodyWithString(value: String): String { + return """{ "p": { "u": "https://cdn-global.configcat.com", "s": "test-salt" }, "f": { "fakeKey": { "t": 1, "v": { "s": "$value" }, "p": [], "r": [], "a":"" }}, "s": [] }""" + } + + fun formatJsonBodyWithBoolean(value: Boolean): String { + return """{ "p": { "u": "https://cdn-global.configcat.com", "s": "test-salt" }, "f": { "fakeKey": { "t": 0, "v": { "b": $value }, "p": [], "r": [], "a":"" }}, "s": [] }""" + } + + fun formatJsonBodyWithInt(value: Int): String { + return """{ "p": { "u": "https://cdn-global.configcat.com", "s": "test-salt" }, "f": { "fakeKey": { "t": 2, "v": { "i": $value }, "p": [], "r": [], "a":"" }}, "s": [] }""" + } + + fun formatJsonBodyWithDouble(value: Double): String { + return """{ "p": { "u": "https://cdn-global.configcat.com", "s": "test-salt" }, "f": { "fakeKey": { "t": 3, "v": { "d": $value }, "p": [], "r": [], "a":"" }}, "s": [] }""" } fun formatConfigWithRules(): String { val config = Config( - null, + Preferences("https://cdn-global.configcat.com", 0, "test-salt"), mapOf( "key" to Setting( - value = "default", - variationId = "defaultId", - rolloutRules = listOf( - RolloutRule( - comparator = 2, - comparisonAttribute = "Identifier", - comparisonValue = "@test1.com", - value = "fake1", - variationId = "fakeId1" + type = 1, + percentageAttribute = "", + percentageOptions = null, + targetingRules = arrayOf( + TargetingRule( + conditions = arrayOf( + Condition( + UserCondition( + comparator = 2, + comparisonAttribute = "Identifier", + stringValue = null, + doubleValue = null, + stringArrayValue = arrayOf("@test1.com") + ), + null, + null + ) + ), + emptyArray(), + servedValue = ServedValue( + value = SettingValue(stringValue = "fake1"), + variationId = "fakeId1" + ) ), - RolloutRule( - comparator = 2, - comparisonAttribute = "Identifier", - comparisonValue = "@test2.com", - value = "fake2", - variationId = "fakeId2" + TargetingRule( + conditions = arrayOf( + Condition( + UserCondition( + comparator = 2, + comparisonAttribute = "Identifier", + stringValue = null, + doubleValue = null, + stringArrayValue = arrayOf("@test2.com") + ), + null, + null + ) + ), + emptyArray(), + servedValue = ServedValue( + value = SettingValue(stringValue = "fake2"), + variationId = "fakeId2" + ) ) - ) + ), + settingValue = SettingValue(stringValue = "default"), + variationId = "defaultId" ) - ) + ), + arrayOf() ) return Constants.json.encodeToString(config) } fun formatCacheEntry(value: Any): String { val fetchTimeUnixSeconds = DateTime.now().unixMillis.toLong() - return "${fetchTimeUnixSeconds}\n$value\n" + """{"f":{"fakeKey":{"v":$value}}}""" + return "${fetchTimeUnixSeconds}\n$value\n" + """{"p":{"u":"https://cdn-global.configcat.com","r":"0","s": "test-slat"}"f":{"fakeKey":{"v":{"s":"$value"},"t":1,"p":[],"r":[], "a":""}}, "s":[]}""" } - fun formatCacheEntryWithEtag(value: Any, etag: String): String { + fun formatCacheEntryWithETag(value: Any, eTag: String): String { val fetchTimeUnixSeconds = DateTime.now().unixMillis.toLong() - return "${fetchTimeUnixSeconds}\n$etag\n" + """{"f":{"fakeKey":{"v":$value}}}""" + return "${fetchTimeUnixSeconds}\n$eTag\n" + """{"p":{"u":"https://cdn-global.configcat.com","r":"0","s": "test-slat"}"f":{"fakeKey":{"v":{"s":"$value"},"t":1,"p":[],"r":[], "a":""}}, "s":[]}""" } fun formatCacheEntryWithDate(value: Any, time: DateTime): String { val fetchTimeUnixSeconds = time.unixMillis.toLong() - return "${fetchTimeUnixSeconds}\n$value\n" + """{"f":{"fakeKey":{"v":$value}}}""" + return "${fetchTimeUnixSeconds}\n$value\n" + """{"p":{"u":"https://cdn-global.configcat.com","r":"0","s": "test-slat"}"f":{"fakeKey":{"v":{"s":"$value"},"t":1,"p":[],"r":[], "a":""}}, "s":[]}""" } } diff --git a/src/commonTest/kotlin/com/configcat/VariationIdTests.kt b/src/commonTest/kotlin/com/configcat/VariationIdTests.kt index d29b1d61..e7325031 100644 --- a/src/commonTest/kotlin/com/configcat/VariationIdTests.kt +++ b/src/commonTest/kotlin/com/configcat/VariationIdTests.kt @@ -16,28 +16,22 @@ class VariationIdTests { @Test fun testGetVariationId() = runTest { val mockEngine = MockEngine { - respond( - content = variationIdBody, - status = HttpStatusCode.OK - ) + respond(content = variationIdBody, status = HttpStatusCode.OK) } - val client = ConfigCatClient("test") { + val client = ConfigCatClient(Data.SDK_KEY) { httpEngine = mockEngine } - val variationId = client.getValueDetails("key1", "defaultValue").variationId + val variationId = client.getValueDetails("key1", false).variationId assertEquals("fakeId1", variationId) } @Test fun testGetVariationIdNotFound() = runTest { val mockEngine = MockEngine { - respond( - content = variationIdBody, - status = HttpStatusCode.OK - ) + respond(content = variationIdBody, status = HttpStatusCode.OK) } - val client = ConfigCatClient("test") { + val client = ConfigCatClient(Data.SDK_KEY) { httpEngine = mockEngine } @@ -48,31 +42,26 @@ class VariationIdTests { @Test fun testGetAllVariationIds() = runTest { val mockEngine = MockEngine { - respond( - content = variationIdBody, - status = HttpStatusCode.OK - ) + respond(content = variationIdBody, status = HttpStatusCode.OK) } - val client = ConfigCatClient("test") { + val client = ConfigCatClient(Data.SDK_KEY) { httpEngine = mockEngine } val allValueDetails = client.getAllValueDetails() - assertEquals(2, allValueDetails.size) + assertEquals(3, allValueDetails.size) assertEquals("fakeId1", allValueDetails.elementAt(0).variationId) assertEquals("fakeId2", allValueDetails.elementAt(1).variationId) + assertEquals("fakeId3", allValueDetails.elementAt(2).variationId) } @Test fun testGetAllVariationIdsEmpty() = runTest { val mockEngine = MockEngine { - respond( - content = "{}", - status = HttpStatusCode.OK - ) + respond(content = "{}", status = HttpStatusCode.OK) } - val client = ConfigCatClient("test") { + val client = ConfigCatClient(Data.SDK_KEY) { httpEngine = mockEngine } @@ -83,12 +72,9 @@ class VariationIdTests { @Test fun testGetKeyAndValue() = runTest { val mockEngine = MockEngine { - respond( - content = variationIdBody, - status = HttpStatusCode.OK - ) + respond(content = variationIdBody, status = HttpStatusCode.OK) } - val client = ConfigCatClient("test") { + val client = ConfigCatClient(Data.SDK_KEY) { httpEngine = mockEngine } @@ -103,17 +89,30 @@ class VariationIdTests { val kv3 = client.getKeyAndValue("rolloutId1") assertEquals("key1", kv3?.first) assertTrue(kv3?.second as? Boolean ?: false) + + val kv4 = client.getKeyAndValue("targetPercentageId2") + assertEquals("key3", kv4?.first) + assertFalse(kv4?.second as? Boolean ?: true) + } + + @Test + fun testGetKeyAndValueIncorrectTargetingRule() = runTest { + val mockEngine = MockEngine { + respond(content = variationIdIncorrectTargetingRuleBody, status = HttpStatusCode.OK) + } + val client = ConfigCatClient(Data.SDK_KEY) { + httpEngine = mockEngine + } + + assertNull(client.getKeyAndValue("targetPercentageId2")) } @Test fun testGetKeyAndValueNotFound() = runTest { val mockEngine = MockEngine { - respond( - content = "{}", - status = HttpStatusCode.OK - ) + respond(content = "{}", status = HttpStatusCode.OK) } - val client = ConfigCatClient("test") { + val client = ConfigCatClient(Data.SDK_KEY) { httpEngine = mockEngine } @@ -121,47 +120,158 @@ class VariationIdTests { } companion object { + const val variationIdIncorrectTargetingRuleBody = """ +{ + "p":{ + "u":"https://cdn-global.configcat.com", + "r":"0", + "s":"PJUt0np9JA4ukMciF3BVAVRJiwIjTOiX\u002BE8B1HQohck=" + }, + "f":{ + "incorrect":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@configcat.com" + ] + } + } + ] + } + ], + "v":{ + "b":false + }, + "i":"incorrectId" + } + } +} + """ + const val variationIdBody = """ - {"f":{ - "key1":{ - "v":true, - "i":"fakeId1", - "p":[ - { - "v":true, - "p":50, - "i":"percentageId1" - }, - { - "v":false, - "p":50, - "i":"percentageId2" - } - ], - "r":[ - { - "a":"Email", - "t":2, - "c":"@configcat.com", - "v":true, - "i":"rolloutId1" - }, - { - "a":"Email", - "t":2, - "c":"@test.com", - "v":false, - "i":"rolloutId2" - } - ] - }, - "key2":{ - "v":false, - "i":"fakeId2", - "p":[], - "r":[] - } - }} +{ + "p":{ + "u":"https://cdn-global.configcat.com", + "r":"0", + "s":"PJUt0np9JA4ukMciF3BVAVRJiwIjTOiX\u002BE8B1HQohck=" + }, + "f":{ + "key1":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"rolloutId1" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@test.com" + ] + } + } + ], + "s":{ + "v":{ + "b":false + }, + "i":"rolloutId2" + } + } + ], + "p":[ + { + "p":50, + "v":{ + "b":true + }, + "i":"percentageId1" + }, + { + "p":50, + "v":{ + "b":false + }, + "i":"percentageId2" + } + ], + "v":{ + "b":true + }, + "i":"fakeId1" + }, + "key2":{ + "t":0, + "v":{ + "b":false + }, + "i":"fakeId2" + }, + "key3":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@configcat.com" + ] + } + } + ], + "p":[ + { + "p":50, + "v":{ + "b":true + }, + "i":"targetPercentageId1" + }, + { + "p":50, + "v":{ + "b":false + }, + "i":"targetPercentageId2" + } + ] + } + ], + "v":{ + "b":false + }, + "i":"fakeId3" + } + } +} """ } } diff --git a/src/commonTest/kotlin/com/configcat/evaluation/EvaluationLoggerTurnOffTests.kt b/src/commonTest/kotlin/com/configcat/evaluation/EvaluationLoggerTurnOffTests.kt new file mode 100644 index 00000000..6a01f2bb --- /dev/null +++ b/src/commonTest/kotlin/com/configcat/evaluation/EvaluationLoggerTurnOffTests.kt @@ -0,0 +1,662 @@ +package com.configcat.evaluation + +import com.configcat.ConfigCatClient +import com.configcat.log.LogLevel +import com.configcat.manualPoll +import io.ktor.client.engine.mock.* +import io.ktor.http.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals + +@OptIn(ExperimentalCoroutinesApi::class) +class EvaluationLoggerTurnOffTests { + // Test cases based on EvaluationTest 1_rule_no_user test case. + private val jsonOverride = """ + { + "p":{ + "u":"https://cdn-global.configcat.com", + "r":0, + "s":"pkw2BWOIXiTrXO53/OPECHP9OeJzmW8y/yV47\u002BQ8HLM=" + }, + "f":{ + "bool30TrueAdvancedRules":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":0, + "l":[ + "a@configcat.com", + "b@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "b":false + }, + "i":"385d9803" + } + }, + { + "c":[ + { + "u":{ + "a":"Country", + "c":2, + "l":[ + "United" + ] + } + } + ], + "s":{ + "v":{ + "b":false + }, + "i":"385d9803" + } + } + ], + "p":[ + { + "p":30, + "v":{ + "b":true + }, + "i":"607147d5" + }, + { + "p":70, + "v":{ + "b":false + }, + "i":"385d9803" + } + ], + "v":{ + "b":true + }, + "i":"607147d5" + }, + "boolDefaultFalse":{ + "t":0, + "v":{ + "b":false + }, + "i":"489a16d2" + }, + "boolDefaultTrue":{ + "t":0, + "v":{ + "b":true + }, + "i":"09513143" + }, + "double25Pi25E25Gr25Zero":{ + "t":3, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "d":5.561 + }, + "i":"3f7826de" + } + } + ], + "p":[ + { + "p":25, + "v":{ + "d":3.1415 + }, + "i":"6d75b4d3" + }, + { + "p":25, + "v":{ + "d":2.7182 + }, + "i":"183ee713" + }, + { + "p":25, + "v":{ + "d":1.61803 + }, + "i":"01eb6326" + }, + { + "p":25, + "v":{ + "d":0 + }, + "i":"64c434ff" + } + ], + "v":{ + "d":-1 + }, + "i":"9503a1de" + }, + "doubleDefaultPi":{ + "t":3, + "v":{ + "d":3.1415 + }, + "i":"5af8acc7" + }, + "integer25One25Two25Three25FourAdvancedRules":{ + "t":2, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "i":5 + }, + "i":"58136ba2" + } + } + ], + "p":[ + { + "p":25, + "v":{ + "i":1 + }, + "i":"11634414" + }, + { + "p":25, + "v":{ + "i":2 + }, + "i":"5530655d" + }, + { + "p":25, + "v":{ + "i":3 + }, + "i":"2ad19a52" + }, + { + "p":25, + "v":{ + "i":4 + }, + "i":"41b30851" + } + ], + "v":{ + "i":-1 + }, + "i":"ce3c4f5a" + }, + "integerDefaultOne":{ + "t":2, + "v":{ + "i":1 + }, + "i":"faadbf54" + }, + "keySampleText":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Country", + "c":0, + "l":[ + "Hungary", + "Bahamas" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"9fa0e57e" + } + }, + { + "c":[ + { + "u":{ + "a":"SubscriptionType", + "c":0, + "l":[ + "unlimited" + ] + } + } + ], + "s":{ + "v":{ + "s":"Lion" + }, + "i":"2be6b03f" + } + } + ], + "p":[ + { + "p":50, + "v":{ + "s":"Falcon" + }, + "i":"baff2362" + }, + { + "p":50, + "v":{ + "s":"Horse" + }, + "i":"dab78ba5" + } + ], + "v":{ + "s":"Cat" + }, + "i":"69ef126c" + }, + "string25Cat25Dog25Falcon25Horse":{ + "t":1, + "p":[ + { + "p":25, + "v":{ + "s":"Cat" + }, + "i":"d227b334" + }, + { + "p":25, + "v":{ + "s":"Dog" + }, + "i":"622f5d07" + }, + { + "p":25, + "v":{ + "s":"Falcon" + }, + "i":"0ff32bab" + }, + { + "p":25, + "v":{ + "s":"Horse" + }, + "i":"6c597441" + } + ], + "v":{ + "s":"Chicken" + }, + "i":"2588a3e6" + }, + "string25Cat25Dog25Falcon25HorseAdvancedRules":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Country", + "c":0, + "l":[ + "Hungary", + "United Kingdom" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dolphin" + }, + "i":"3accb1d0" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":2, + "l":[ + "admi" + ] + } + } + ], + "s":{ + "v":{ + "s":"Lion" + }, + "i":"e95ebf10" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Kitten" + }, + "i":"88243650" + } + } + ], + "p":[ + { + "p":25, + "v":{ + "s":"Cat" + }, + "i":"83461b47" + }, + { + "p":25, + "v":{ + "s":"Dog" + }, + "i":"4f026fbc" + }, + { + "p":25, + "v":{ + "s":"Falcon" + }, + "i":"392a4d59" + }, + { + "p":25, + "v":{ + "s":"Horse" + }, + "i":"bb66b1f3" + } + ], + "v":{ + "s":"Chicken" + }, + "i":"8250ef5a" + }, + "string75Cat0Dog25Falcon0Horse":{ + "t":1, + "p":[ + { + "p":75, + "v":{ + "s":"Cat" + }, + "i":"93f5a1c0" + }, + { + "p":0, + "v":{ + "s":"Dog" + }, + "i":"b8f49554" + }, + { + "p":25, + "v":{ + "s":"Falcon" + }, + "i":"7beaf504" + }, + { + "p":0, + "v":{ + "s":"Horse" + }, + "i":"30ee31af" + } + ], + "v":{ + "s":"Chicken" + }, + "i":"aa65b5ce" + }, + "stringContainsDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"d0cd8f06" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"ce564c3a" + }, + "stringDefaultCat":{ + "t":1, + "v":{ + "s":"Cat" + }, + "i":"7a0be518" + }, + "stringIsInDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":0, + "l":[ + "a@configcat.com", + "b@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"5b64d9b4" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":0, + "l":[ + "admin" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"5b64d9b4" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"83372510" + }, + "stringIsNotInDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":1, + "l":[ + "a@configcat.com", + "b@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"6ada5ff2" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"2459598d" + }, + "stringNotContainsDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":3, + "l":[ + "@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"f7f8f43d" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"44ab483a" + } + } +} + """.trimIndent() + + private val mockEngine = MockEngine { + respond(content = jsonOverride, status = HttpStatusCode.OK, headersOf(Pair("ETag", listOf("fakeETag")))) + } + + @Test + fun testEvaluationLogLevelInfo() = runTest { + // based on 1_rule_no_user test case. + val evaluationTestLogger = EvaluationTestLogger() + + val client = ConfigCatClient("PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A") { + pollingMode = manualPoll() + logger = evaluationTestLogger + logLevel = LogLevel.INFO + httpEngine = mockEngine + } + client.forceRefresh() + + val result: Any? = client.getAnyValue("stringContainsDogDefaultCat", "default", null) + + val logList = evaluationTestLogger.getLogList() + assertEquals("Cat", result, "Return value not match.") + assertEquals(2, evaluationTestLogger.getLogList().size, "Logged event size not match.") + assertEquals(LogLevel.WARNING, logList[0].logLevel, "Logged event level not match.") + assertEquals(LogLevel.INFO, logList[1].logLevel, "Logged event level not match.") + + evaluationTestLogger.resetLogList() + client.close() + } + + @Test + fun testEvaluationLogLevelWarning() = runTest { + // based on 1_rule_no_user test case. + val evaluationTestLogger = EvaluationTestLogger() + val client = ConfigCatClient("PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A") { + pollingMode = manualPoll() + logger = evaluationTestLogger + logLevel = LogLevel.WARNING + httpEngine = mockEngine + } + client.forceRefresh() + + val result: Any? = client.getAnyValue("stringContainsDogDefaultCat", "default", null) + + val logList = evaluationTestLogger.getLogList() + assertEquals("Cat", result, "Return value not match.") + assertEquals(1, logList.size, "Logged event size not match. ") + assertEquals(LogLevel.WARNING, logList[0].logLevel, "Logged event level not match.") + + evaluationTestLogger.resetLogList() + client.close() + } +} diff --git a/src/commonTest/kotlin/com/configcat/evaluation/EvaluationTestLogger.kt b/src/commonTest/kotlin/com/configcat/evaluation/EvaluationTestLogger.kt new file mode 100644 index 00000000..0abc4125 --- /dev/null +++ b/src/commonTest/kotlin/com/configcat/evaluation/EvaluationTestLogger.kt @@ -0,0 +1,53 @@ +package com.configcat.evaluation + +import com.configcat.log.LogLevel +import com.configcat.log.Logger + +class EvaluationTestLogger : Logger { + private var logList: ArrayList = arrayListOf() + + private val levelMap: HashMap = hashMapOf( + LogLevel.ERROR to "ERROR", + LogLevel.WARNING to "WARNING", + LogLevel.INFO to "INFO", + LogLevel.DEBUG to "DEBUG" + ) + + override fun error(message: String) { + logMessage(enrichMessage(message, LogLevel.ERROR), LogLevel.ERROR) + } + + override fun error(message: String, throwable: Throwable) { + logMessage(enrichMessage("$message $throwable", LogLevel.ERROR), LogLevel.ERROR) + } + + override fun warning(message: String) { + logMessage(enrichMessage(message, LogLevel.WARNING), LogLevel.WARNING) + } + + override fun info(message: String) { + logMessage(enrichMessage(message, LogLevel.INFO), LogLevel.INFO) + } + + override fun debug(message: String) { + logMessage(enrichMessage(message, LogLevel.DEBUG), LogLevel.DEBUG) + } + + private fun logMessage(message: String, logLevel: LogLevel) { + logList.add(LogEvent(logLevel, message)) + } + + fun getLogList(): List { + return logList + } + + fun resetLogList() { + logList = arrayListOf() + } + + private fun enrichMessage(message: String, level: LogLevel): String { + return "${levelMap[level]} $message" + } +} + +class LogEvent(val logLevel: LogLevel, val logMessage: String) diff --git a/src/commonTest/kotlin/com/configcat/evaluation/EvaluationTests.kt b/src/commonTest/kotlin/com/configcat/evaluation/EvaluationTests.kt new file mode 100644 index 00000000..4c44ef7b --- /dev/null +++ b/src/commonTest/kotlin/com/configcat/evaluation/EvaluationTests.kt @@ -0,0 +1,164 @@ +package com.configcat.evaluation + +import com.configcat.ConfigCatClient +import com.configcat.SingleValueCache +import com.configcat.evaluation.data.* +import com.configcat.log.LogLevel +import com.configcat.manualPoll +import io.ktor.client.engine.mock.* +import io.ktor.http.* +import io.ktor.util.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.fail + +@OptIn(ExperimentalCoroutinesApi::class) +class EvaluationTests { + + @Test + fun testSimpleValue() = runTest { + testEvaluation(SimpleValueTests) + } + + @Test + fun testOneTargetRule() = runTest { + testEvaluation(OneTargetingRuleTests) + } + + @Test + fun testTwoTargetingRules() = runTest { + testEvaluation(TwoTargetingRulesTests) + } + + @Test + fun testAndRules() = runTest { + testEvaluation(AndRulesTests) + } + + @Test + fun testComparators() = runTest { + // Native test run separately for this test + if (PlatformUtils.IS_NATIVE) { + return@runTest + } + testEvaluation(ComparatorsTests) + } + + @Test + fun testSemverValidation() = runTest { + testEvaluation(SemverValidationTests) + } + + @Test + fun testNumberValidation() = runTest { + testEvaluation(NumberValidationTests) + } + + @Test + fun testEpochDateValidation() = runTest { + // Native test run separately for this test + if (PlatformUtils.IS_NATIVE) { + return@runTest + } + testEvaluation(EpochDateValidationTests) + } + + @Test + fun testPrerequisiteFlag() = runTest { + testEvaluation(PrerequisiteFlagTests) + } + + @Test + fun testSegment() = runTest { + testEvaluation(SegmentTests) + } + + @Test + fun testOptionsAfterTargetingRule() = runTest { + testEvaluation(OptionsAfterTargetingRuleTests) + } + + @Test + fun testOptionsBasedOnUserId() = runTest { + testEvaluation(OptionsBasedOnUserIdTests) + } + + @Test + fun testOptionsBasedOnCustomAttr() = runTest { + testEvaluation(OptionsBasedOnCustomAttrTests) + } + + @Test + fun testOptionsWithinTargetingRule() = runTest { + testEvaluation(OptionsWithinTargetingRuleTests) + } + + @Test + fun testListTruncation() = runTest { + testEvaluation(ListTruncationTests) + } + + private suspend fun testEvaluation(testSet: TestSet) { + var sdkKey = testSet.sdkKey + if (sdkKey.isNullOrEmpty()) { + sdkKey = TEST_SDK_KEY + } + + val mockEngine = MockEngine { + respond( + content = testSet.jsonOverride, + status = HttpStatusCode.OK, + headersOf(Pair("ETag", listOf("fakeETag"))) + ) + } + + val evaluationTestLogger = EvaluationTestLogger() + val client = ConfigCatClient(sdkKey) { + pollingMode = manualPoll() + baseUrl = testSet.baseUrl + httpEngine = mockEngine + logger = evaluationTestLogger + logLevel = LogLevel.INFO + + // add empty SingleValueCache to avoid JS extra cache logs + configCache = SingleValueCache("") + } + client.forceRefresh() + + val tests = testSet.tests + val errors: ArrayList = arrayListOf() + for (test in tests!!) { + val settingKey = test.key + + val result: Any? = client.getAnyValue(settingKey, test.defaultValue, test.user) + if (test.returnValue != result) { + errors.add("Return value mismatch for test: %s Test Key: $settingKey Expected: ${test.returnValue}, Result: $result \n") + } + val expectedLog = test.expectedLog + val logResultBuilder = StringBuilder() + val logsList = evaluationTestLogger.getLogList() + for (i in logsList.indices) { + val log = logsList[i] + logResultBuilder.append(log.logMessage) + if (i != logsList.size - 1) { + logResultBuilder.append("\n") + } + } + val logResult: String = logResultBuilder.toString() + if (expectedLog != logResult) { + errors.add("Log mismatch for test: %s Test Key: $settingKey Expected:\n$expectedLog\nResult:\n$logResult\n") + } + evaluationTestLogger.resetLogList() + } + if (errors.isNotEmpty()) { + println("\n == ERRORS == \n") + fail(errors.joinToString("\n")) + } + client.close() + } + + companion object { + private const val TEST_SDK_KEY = "configcat-sdk-test-key/0000000000000000000000" + } +} diff --git a/src/commonTest/kotlin/com/configcat/evaluation/data/AndRulesTests.kt b/src/commonTest/kotlin/com/configcat/evaluation/data/AndRulesTests.kt new file mode 100644 index 00000000..eb057d90 --- /dev/null +++ b/src/commonTest/kotlin/com/configcat/evaluation/data/AndRulesTests.kt @@ -0,0 +1,463 @@ +package com.configcat.evaluation.data + +import com.configcat.ConfigCatUser + +object AndRulesTests : TestSet { + override val sdkKey = "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/ByMO9yZNn02kXcm72lnY1A" + override val baseUrl = null + override val jsonOverride = """ + { + "p":{ + "u":"https://cdn-global.configcat.com", + "r":0, + "s":"XYA\u002BaDHULdih3lcV/DfSC4I3HgX3hkoYC5Te638DGSU=" + }, + "s":[ + { + "n":"Beta Users", + "r":[ + { + "a":"Email", + "c":16, + "l":[ + "5a1c36ec9cb651709b85f7295405880dc4728d3c5b27b4de09476bba2c10553b", + "83eea4b4f01b5471a1bf2505a1d141485fc29e576e741889a30bf6555ad02b01" + ] + } + ] + }, + { + "n":"Developers", + "r":[ + { + "a":"Email", + "c":16, + "l":[ + "3b578b8bd4998b1b3c042b3a28746c4ff1d41990a535e941a826564e1d45b523", + "16cb6133c8bdc3e6d427f96f818a2b1c3a274e62c5a37ec42c4a47184ee7c54b" + ] + } + ] + } + ], + "f":{ + "dependentFeature":{ + "t":1, + "r":[ + { + "c":[ + { + "p":{ + "f":"mainFeature", + "c":0, + "v":{ + "s":"target" + } + } + } + ], + "p":[ + { + "p":25, + "v":{ + "s":"Cat" + }, + "i":"993d7ee0" + }, + { + "p":25, + "v":{ + "s":"Dog" + }, + "i":"08b8348e" + }, + { + "p":25, + "v":{ + "s":"Falcon" + }, + "i":"a6fb7a01" + }, + { + "p":25, + "v":{ + "s":"Horse" + }, + "i":"699fb4bf" + } + ] + } + ], + "v":{ + "s":"Chicken" + }, + "i":"e6198f92" + }, + "dependentFeatureWithUserCondition":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":16, + "l":[ + "c904e7bad986f101a63a5da52cdf4fbb9660bea2ad79dccbd6121075ab610b1f", + "84da58e3ca0ff12167c80d39731b391f9ecd8003af693b01240897b8453f0f43" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"ef802e43" + } + }, + { + "c":[ + { + "p":{ + "f":"mainFeatureWithoutUserCondition", + "c":0, + "v":{ + "b":true + } + } + } + ], + "p":[ + { + "p":34, + "v":{ + "s":"Cat" + }, + "i":"4a65d6ef" + }, + { + "p":33, + "v":{ + "s":"Horse" + }, + "i":"fc3bb22b" + }, + { + "p":33, + "v":{ + "s":"Falcon" + }, + "i":"32e0e525" + } + ] + } + ], + "v":{ + "s":"Chicken" + }, + "i":"472e10f4" + }, + "dependentFeatureWithUserCondition2":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":16, + "l":[ + "38b591c3f8a1434d092ad362b8a3f76625938d7937cdc5bb67bda2ccc474df94", + "2f985eef0a5f9b5193c88006e7ec1bb70cd4ea375273415254e538e33e033026" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"78eceed0" + } + }, + { + "c":[ + { + "p":{ + "f":"mainFeature", + "c":0, + "v":{ + "s":"public" + } + } + } + ], + "p":[ + { + "p":34, + "v":{ + "s":"Cat" + }, + "i":"72b97d0e" + }, + { + "p":33, + "v":{ + "s":"Horse" + }, + "i":"81846c69" + }, + { + "p":33, + "v":{ + "s":"Falcon" + }, + "i":"e2f3b509" + } + ] + }, + { + "c":[ + { + "p":{ + "f":"mainFeature", + "c":0, + "v":{ + "s":"public" + } + } + } + ], + "s":{ + "v":{ + "s":"Frog" + }, + "i":"cfd56c79" + } + } + ], + "v":{ + "s":"Chicken" + }, + "i":"9e8d62c6" + }, + "emailAnd":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":22, + "l":[ + "4_76c49f275a4016de6c7a2464942df853891f0e7c39f176136abdf9edf6f3ffd4" + ] + } + }, + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@" + ] + } + }, + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "20_54899f4b5464250fe02af0fc09f5bf863865bb543c9f981133151b7fcc133bb7" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"a1393561" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"bdabd589" + }, + "emailOr":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":22, + "l":[ + "5_a366d82b39567d7d60654732b75973217f90cd8b3e8cbd89f8e9247257f8f421" + ] + } + } + ], + "s":{ + "v":{ + "s":"Jane" + }, + "i":"01383bbf" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":22, + "l":[ + "5_b587748cc92c5f3f3f5792e82b4cb93efaab7b2211aa7d1102e78a22589715e1" + ] + } + } + ], + "s":{ + "v":{ + "s":"John" + }, + "i":"a069dc24" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":22, + "l":[ + "5_670b198d20a45c499f5a32b1a46ee1e4f13d1c42594b3096e47daf340d8fb8e0" + ] + } + } + ], + "s":{ + "v":{ + "s":"Mark" + }, + "i":"d7b02cc0" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"ab0b46ad" + }, + "mainFeature":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "21_56a25e5b7dc4bff5f0634b52ec41b2e1d0ed838c7297a83967cd7aa1e14bc36a" + ] + } + }, + { + "u":{ + "a":"Country", + "c":16, + "l":[ + "1fab893a891c70917518e97fb4b0be62aab1888d305a9c8f393a201a83900bc0", + "0d8f106bc09eef0f6aa4a36f861b4b30c17186623543ac69bd654c9a931cd42f" + ] + } + } + ], + "s":{ + "v":{ + "s":"private" + }, + "i":"64f8e1a6" + } + }, + { + "c":[ + { + "u":{ + "a":"Country", + "c":16, + "l":[ + "d3be141e87c1ed4c35330fc7d7c37f617e98d4c17ee7e8be739c87ca07aa048c" + ] + } + }, + { + "s":{ + "s":0, + "c":1 + } + }, + { + "s":{ + "s":1, + "c":1 + } + } + ], + "s":{ + "v":{ + "s":"target" + }, + "i":"f570ef26" + } + } + ], + "v":{ + "s":"public" + }, + "i":"f16ac582" + }, + "mainFeatureWithoutUserCondition":{ + "t":0, + "v":{ + "b":true + }, + "i":"1c6ca36e" + } + } +}""" + override val tests: Array = arrayOf( + TestCase( + key = "emailAnd", + defaultValue = "default", + returnValue = "Cat", + user = null, + expectedLog = """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'.""" + ), + TestCase( + key = "emailAnd", + defaultValue = "default", + returnValue = "Cat", + user = ConfigCatUser("12345", "jane@configcat.com"), + expectedLog = """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/src/commonTest/kotlin/com/configcat/evaluation/data/ComparatorsTests.kt b/src/commonTest/kotlin/com/configcat/evaluation/data/ComparatorsTests.kt new file mode 100644 index 00000000..9392a5fb --- /dev/null +++ b/src/commonTest/kotlin/com/configcat/evaluation/data/ComparatorsTests.kt @@ -0,0 +1,1457 @@ +package com.configcat.evaluation.data + +import com.configcat.ConfigCatUser + +object ComparatorsTests : TestSet { + override val sdkKey = "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ" + override val baseUrl = null + override val jsonOverride = """ + { + "p":{ + "u":"https://cdn-global.configcat.com", + "r":0, + "s":"JEl\u002BhoGfr/01JCnpxr7kOCIoB2bYAM3uTMShm6HiAc4=" + }, + "f":{ + "allinone":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":20, + "s":"0740e9103e26acb85b086dd9a15eaa84246902f096655371a5f001b9e13754e8" + } + }, + { + "u":{ + "a":"Email", + "c":21, + "s":"0740e9103e26acb85b086dd9a15eaa84246902f096655371a5f001b9e13754e8" + } + } + ], + "s":{ + "v":{ + "s":"1h" + }, + "i":"e3a79156" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":28, + "s":"joe@example.com" + } + }, + { + "u":{ + "a":"Email", + "c":29, + "s":"joe@example.com" + } + } + ], + "s":{ + "v":{ + "s":"1c" + }, + "i":"ed60451a" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":16, + "l":[ + "0740e9103e26acb85b086dd9a15eaa84246902f096655371a5f001b9e13754e8" + ] + } + }, + { + "u":{ + "a":"Email", + "c":17, + "l":[ + "0740e9103e26acb85b086dd9a15eaa84246902f096655371a5f001b9e13754e8" + ] + } + } + ], + "s":{ + "v":{ + "s":"2h" + }, + "i":"aa24b7a3" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":0, + "l":[ + "joe@example.com" + ] + } + }, + { + "u":{ + "a":"Email", + "c":1, + "l":[ + "joe@example.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"2c" + }, + "i":"d37425a1" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":22, + "l":[ + "4_e99c716658ca0b1035394161a3ca54f8dc688930ad90bed26aeff075cb947397" + ] + } + }, + { + "u":{ + "a":"Email", + "c":23, + "l":[ + "4_e99c716658ca0b1035394161a3ca54f8dc688930ad90bed26aeff075cb947397" + ] + } + } + ], + "s":{ + "v":{ + "s":"3h" + }, + "i":"5e6e0c6c" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":30, + "l":[ + "joe@" + ] + } + }, + { + "u":{ + "a":"Email", + "c":31, + "l":[ + "joe@" + ] + } + } + ], + "s":{ + "v":{ + "s":"3c" + }, + "i":"5f562a70" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "12_29030906a5c2729247ccad10154b56b84d61ee4d732361e0ba7c3817da4f91b3" + ] + } + }, + { + "u":{ + "a":"Email", + "c":25, + "l":[ + "12_29030906a5c2729247ccad10154b56b84d61ee4d732361e0ba7c3817da4f91b3" + ] + } + } + ], + "s":{ + "v":{ + "s":"4h" + }, + "i":"91b91d69" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":32, + "l":[ + "@example.com" + ] + } + }, + { + "u":{ + "a":"Email", + "c":33, + "l":[ + "@example.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"4c" + }, + "i":"4c80a977" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "e@e" + ] + } + }, + { + "u":{ + "a":"Email", + "c":3, + "l":[ + "e@e" + ] + } + } + ], + "s":{ + "v":{ + "s":"5" + }, + "i":"dd12c429" + } + }, + { + "c":[ + { + "u":{ + "a":"Version", + "c":4, + "l":[ + "1.0.0" + ] + } + }, + { + "u":{ + "a":"Version", + "c":5, + "l":[ + "1.0.0" + ] + } + } + ], + "s":{ + "v":{ + "s":"6" + }, + "i":"dba5d266" + } + }, + { + "c":[ + { + "u":{ + "a":"Version", + "c":6, + "s":"1.0.1" + } + }, + { + "u":{ + "a":"Version", + "c":9, + "s":"1.0.1" + } + } + ], + "s":{ + "v":{ + "s":"7" + }, + "i":"1637ffc5" + } + }, + { + "c":[ + { + "u":{ + "a":"Version", + "c":8, + "s":"0.9.9" + } + }, + { + "u":{ + "a":"Version", + "c":7, + "s":"0.9.9" + } + } + ], + "s":{ + "v":{ + "s":"8" + }, + "i":"b084ddd6" + } + }, + { + "c":[ + { + "u":{ + "a":"Number", + "c":10, + "d":1 + } + }, + { + "u":{ + "a":"Number", + "c":11, + "d":1 + } + } + ], + "s":{ + "v":{ + "s":"9" + }, + "i":"d1d537a6" + } + }, + { + "c":[ + { + "u":{ + "a":"Number", + "c":12, + "d":1.1 + } + }, + { + "u":{ + "a":"Number", + "c":15, + "d":1.1 + } + } + ], + "s":{ + "v":{ + "s":"10" + }, + "i":"52c846d0" + } + }, + { + "c":[ + { + "u":{ + "a":"Number", + "c":14, + "d":0.9 + } + }, + { + "u":{ + "a":"Number", + "c":13, + "d":0.9 + } + } + ], + "s":{ + "v":{ + "s":"11" + }, + "i":"c91ffb7c" + } + }, + { + "c":[ + { + "u":{ + "a":"Date", + "c":18, + "d":1693497600 + } + }, + { + "u":{ + "a":"Date", + "c":19, + "d":1693497600 + } + } + ], + "s":{ + "v":{ + "s":"12" + }, + "i":"c12182ef" + } + }, + { + "c":[ + { + "u":{ + "a":"Country", + "c":26, + "l":[ + "5a85699e7343a36d89ee75dca859f7a73cb6be89182095bffb021d1d78de046c" + ] + } + }, + { + "u":{ + "a":"Country", + "c":27, + "l":[ + "5a85699e7343a36d89ee75dca859f7a73cb6be89182095bffb021d1d78de046c" + ] + } + } + ], + "s":{ + "v":{ + "s":"13h" + }, + "i":"a16b1a17" + } + }, + { + "c":[ + { + "u":{ + "a":"Country", + "c":34, + "l":[ + "USA" + ] + } + }, + { + "u":{ + "a":"Country", + "c":35, + "l":[ + "USA" + ] + } + } + ], + "s":{ + "v":{ + "s":"13c" + }, + "i":"1a17d1b3" + } + } + ], + "v":{ + "s":"default" + }, + "i":"9ff25f81" + }, + "arrayContainsCaseCheckDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":26, + "l":[ + "00083f86e0f648b23f6721d43033bbef14378266fbf7de8a6760cc2ad237e9f3" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"5d80eff1" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"ce055a38" + }, + "arrayContainsDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":26, + "l":[ + "c2d5024661bc0e13f769cbb28bbfab7b78dac88b7876f007020f2e7cd47b1114" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"147fdd01" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"5f573f9c" + }, + "arrayDoesNotContainCaseCheckDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":27, + "l":[ + "14748140c64ecab48c7fd13f03811fe6390c8f578c99df96cf36fc2c6152f660" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"d4ad5730" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"df4915fd" + }, + "arrayDoesNotContainDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":27, + "l":[ + "bc0c24462c5098e434c63c7fcc6343af5000a4e7affd309f365edd4ccb7f428b" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"c2161ac9" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"41910880" + }, + "boolTrueIn202304":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":19, + "d":1680307200 + } + }, + { + "u":{ + "a":"Custom1", + "c":18, + "d":1682899200 + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"6948d7cd" + } + } + ], + "v":{ + "b":false + }, + "i":"ae2a09bd" + }, + "countryPercentageAttribute":{ + "t":1, + "a":"Country", + "p":[ + { + "p":50, + "v":{ + "s":"Falcon" + }, + "i":"2b05fd81" + }, + { + "p":50, + "v":{ + "s":"Horse" + }, + "i":"e28b6a82" + } + ], + "v":{ + "s":"Chicken" + }, + "i":"29bb6bbb" + }, + "customPercentageAttribute":{ + "t":1, + "a":"Custom1", + "p":[ + { + "p":50, + "v":{ + "s":"Falcon" + }, + "i":"3715712d" + }, + { + "p":50, + "v":{ + "s":"Horse" + }, + "i":"7b3542d5" + } + ], + "v":{ + "s":"Chicken" + }, + "i":"50466fb6" + }, + "missingPercentageAttribute":{ + "t":1, + "a":"NotFound", + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "14_902a42101e8b77851c98456b383fd959ed0f5aed5b919b4a623c8c756cf0c3ab" + ] + } + } + ], + "p":[ + { + "p":50, + "v":{ + "s":"Falcon" + }, + "i":"4b7d88ba" + }, + { + "p":50, + "v":{ + "s":"Horse" + }, + "i":"a1c2c9a9" + } + ] + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "14_902a42101e8b77851c98456b383fd959ed0f5aed5b919b4a623c8c756cf0c3ab" + ] + } + } + ], + "s":{ + "v":{ + "s":"NotFound" + }, + "i":"8aa042fe" + } + } + ], + "v":{ + "s":"Chicken" + }, + "i":"e5107172" + }, + "stringArrayContainsAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":34, + "l":[ + "read", + "write", + "execute" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"9ddb8a37" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"0d45ab4b" + }, + "stringArrayContainsAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":26, + "l":[ + "090415ce6b462a2152e06d68aeb7c452a564d19a262eb959c510636a189e105d", + "0aa50b49ca02a59cf507856d88ab25b76a7e69e553d25e67254359d8bbb8b1d6", + "1aa91982f7ccae1943f05ac437d635b3261e3ce06aba846dd2ecc6332e4675c2" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"aa03b1ff" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"203317f5" + }, + "stringArrayNotContainsAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":35, + "l":[ + "read", + "write", + "execute" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"15c865df" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"6df210da" + }, + "stringArrayNotContainsAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":27, + "l":[ + "b645a36d4d2e24a0612296ded074eace87ce10a0da21c5cb2ac9a6dccbe79cc5", + "552a92975423ea255384f48a6387be172c05ac72238f90050cdfe27cc4659cfd", + "4c00d4171202dbeb35830b1df8e47185968351c771bb2f633ea50ac1c049e016" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"259816ba" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"60b961b0" + }, + "stringContainsAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@verizon.com", + "@verizon.net", + "@aol.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"09af657f" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"063bcf39" + }, + "stringDoseNotEqualDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":21, + "s":"3cb496a1c3844215c784116ce9e91c143860d7ef18f7fadadcc901b8df5d235c" + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"8e423808" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"1835a09a" + }, + "stringEndsWithAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":32, + "l":[ + "@verizon.com", + "@verizon.net", + "@aol.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"33d35402" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"31976ec3" + }, + "stringEndsWithAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "12_781f5f9b054a14721a835fcdd2d03ac6d45d99eb55b387bf5938904c8f65aa35", + "12_d15f199cfbd96ef1c6f7683e66d5e0f85c9c591ce377289abb2e785fc71bbdf1", + "8_6ee234c513b7518bd705cc1049576c1e757a201200ff26a6a3a91821f954c6cb" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"7231ddf8" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"de17fd2a" + }, + "stringEndsWithDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "14_236ec291c9b54fad3373a0b7e0e465b33459198fd1027838c101516ad8ae1b39" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"d7a00741" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"45b7d922" + }, + "stringEqualsCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":28, + "s":"a@configcat.com" + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"087e01dd" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"89785ab3" + }, + "stringEqualsDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":20, + "s":"86e6b430939acac093f3d8c48be10798896f0abf3b96e2e39080acedd925d887" + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"703c31ed" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"adc0b01c" + }, + "stringNotContainsAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":3, + "l":[ + "@verizon.com", + "@verizon.net", + "@aol.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"49627b36" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"36848b03" + }, + "stringNotEndsWithAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":33, + "l":[ + "@verizon.com", + "@verizon.net", + "@aol.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"886ced9d" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"864b6202" + }, + "stringNotEndsWithAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":25, + "l":[ + "12_295e66dc483034349bc6cffd14190ccff949ac1f34df5fbf01437b8162719b98", + "12_11933417446186b92f63db5931a275f156485d0aef5ee8e265afb350921bd0b1", + "8_aa181c1d9462e3916cb05669e2c408541edf03dad19a9655b340eb32ddd4d060" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"6eb0ec3a" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"7020bcd6" + }, + "stringNotEndsWithDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":25, + "l":[ + "14_141d3f398885dd06d6c14e6629602125d8cb0dddf2ef85896f682c74abb4bc28" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"d37b6f18" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"91ba1bcb" + }, + "stringNotEqualsCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":29, + "s":"b@configcat.com" + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"3ace20fb" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"09c9725f" + }, + "stringNotStartsWithAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":31, + "l":[ + "u@", + "a@", + "b@" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"3717f2ca" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"1d661433" + }, + "stringNotStartsWithAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":23, + "l":[ + "2_b0ab1063bba4431f95710a8bd9c9e2bcd1cebc09fe6803cf87ee501d045e8ee0", + "2_9206a0a6b987410488c8020650788c597509af05aa5327f92cae1c999c401c34", + "2_d8f5c200e2c54b0f84f7df024576f799f484d0e258c6058142a4993daa5e2998" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"b5ba025e" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"c35929e3" + }, + "stringNotStartsWithDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":23, + "l":[ + "1_061a38ed8d5955d90b88d1b1949512a45032390a2581f3c7e36597f7459a48c7" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"72c4e1ac" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"2b16da78" + }, + "stringStartsWithAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":30, + "l":[ + "u@", + "a@", + "b@" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"9e55f5cf" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"e170a185" + }, + "stringStartsWithAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":22, + "l":[ + "2_52f47d1a193071a304e41812055cc1282d90287e5c993ae159fa08fb1ccd3656", + "2_6e5ae1bb586660088d330ac106bbf6b7f2b43be1ca70dbb766f203b76bc84049", + "2_3edba8b738135ad32e85d8695acb8d8511ac67d83f50da55abd9ba469da88efe" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"1d9b7603" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"dd5b3211" + }, + "stringStartsWithDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":22, + "l":[ + "1_55e0b0566d89dc0fda2323efcfb958c782a4648513bdcc4dc84b044fd34230dd" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"3b409872" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"3659b0fe" + } + } +} + """.trimIndent() + override val tests: Array = arrayOf( + TestCase( + key = "allinone", + defaultValue = "", + returnValue = "default", + user = ConfigCatUser( + "12345", + "joe@example.com", + "[\"USA\"]", + custom = mapOf("Version" to "1.0.0", "Number" to "1.0", "Date" to "1693497500") + ), + expectedLog = """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/src/commonTest/kotlin/com/configcat/evaluation/data/EpochDateValidationTests.kt b/src/commonTest/kotlin/com/configcat/evaluation/data/EpochDateValidationTests.kt new file mode 100644 index 00000000..01d5a345 --- /dev/null +++ b/src/commonTest/kotlin/com/configcat/evaluation/data/EpochDateValidationTests.kt @@ -0,0 +1,1402 @@ +package com.configcat.evaluation.data + +import com.configcat.ConfigCatUser + +object EpochDateValidationTests : TestSet { + override val sdkKey = "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ" + override val baseUrl = null + override val jsonOverride = """ + { + "p":{ + "u":"https://cdn-global.configcat.com", + "r":0, + "s":"JEl\u002BhoGfr/01JCnpxr7kOCIoB2bYAM3uTMShm6HiAc4=" + }, + "f":{ + "allinone":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":20, + "s":"0740e9103e26acb85b086dd9a15eaa84246902f096655371a5f001b9e13754e8" + } + }, + { + "u":{ + "a":"Email", + "c":21, + "s":"0740e9103e26acb85b086dd9a15eaa84246902f096655371a5f001b9e13754e8" + } + } + ], + "s":{ + "v":{ + "s":"1h" + }, + "i":"e3a79156" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":28, + "s":"joe@example.com" + } + }, + { + "u":{ + "a":"Email", + "c":29, + "s":"joe@example.com" + } + } + ], + "s":{ + "v":{ + "s":"1c" + }, + "i":"ed60451a" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":16, + "l":[ + "0740e9103e26acb85b086dd9a15eaa84246902f096655371a5f001b9e13754e8" + ] + } + }, + { + "u":{ + "a":"Email", + "c":17, + "l":[ + "0740e9103e26acb85b086dd9a15eaa84246902f096655371a5f001b9e13754e8" + ] + } + } + ], + "s":{ + "v":{ + "s":"2h" + }, + "i":"aa24b7a3" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":0, + "l":[ + "joe@example.com" + ] + } + }, + { + "u":{ + "a":"Email", + "c":1, + "l":[ + "joe@example.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"2c" + }, + "i":"d37425a1" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":22, + "l":[ + "4_e99c716658ca0b1035394161a3ca54f8dc688930ad90bed26aeff075cb947397" + ] + } + }, + { + "u":{ + "a":"Email", + "c":23, + "l":[ + "4_e99c716658ca0b1035394161a3ca54f8dc688930ad90bed26aeff075cb947397" + ] + } + } + ], + "s":{ + "v":{ + "s":"3h" + }, + "i":"5e6e0c6c" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":30, + "l":[ + "joe@" + ] + } + }, + { + "u":{ + "a":"Email", + "c":31, + "l":[ + "joe@" + ] + } + } + ], + "s":{ + "v":{ + "s":"3c" + }, + "i":"5f562a70" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "12_29030906a5c2729247ccad10154b56b84d61ee4d732361e0ba7c3817da4f91b3" + ] + } + }, + { + "u":{ + "a":"Email", + "c":25, + "l":[ + "12_29030906a5c2729247ccad10154b56b84d61ee4d732361e0ba7c3817da4f91b3" + ] + } + } + ], + "s":{ + "v":{ + "s":"4h" + }, + "i":"91b91d69" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":32, + "l":[ + "@example.com" + ] + } + }, + { + "u":{ + "a":"Email", + "c":33, + "l":[ + "@example.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"4c" + }, + "i":"4c80a977" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "e@e" + ] + } + }, + { + "u":{ + "a":"Email", + "c":3, + "l":[ + "e@e" + ] + } + } + ], + "s":{ + "v":{ + "s":"5" + }, + "i":"dd12c429" + } + }, + { + "c":[ + { + "u":{ + "a":"Version", + "c":4, + "l":[ + "1.0.0" + ] + } + }, + { + "u":{ + "a":"Version", + "c":5, + "l":[ + "1.0.0" + ] + } + } + ], + "s":{ + "v":{ + "s":"6" + }, + "i":"dba5d266" + } + }, + { + "c":[ + { + "u":{ + "a":"Version", + "c":6, + "s":"1.0.1" + } + }, + { + "u":{ + "a":"Version", + "c":9, + "s":"1.0.1" + } + } + ], + "s":{ + "v":{ + "s":"7" + }, + "i":"1637ffc5" + } + }, + { + "c":[ + { + "u":{ + "a":"Version", + "c":8, + "s":"0.9.9" + } + }, + { + "u":{ + "a":"Version", + "c":7, + "s":"0.9.9" + } + } + ], + "s":{ + "v":{ + "s":"8" + }, + "i":"b084ddd6" + } + }, + { + "c":[ + { + "u":{ + "a":"Number", + "c":10, + "d":1 + } + }, + { + "u":{ + "a":"Number", + "c":11, + "d":1 + } + } + ], + "s":{ + "v":{ + "s":"9" + }, + "i":"d1d537a6" + } + }, + { + "c":[ + { + "u":{ + "a":"Number", + "c":12, + "d":1.1 + } + }, + { + "u":{ + "a":"Number", + "c":15, + "d":1.1 + } + } + ], + "s":{ + "v":{ + "s":"10" + }, + "i":"52c846d0" + } + }, + { + "c":[ + { + "u":{ + "a":"Number", + "c":14, + "d":0.9 + } + }, + { + "u":{ + "a":"Number", + "c":13, + "d":0.9 + } + } + ], + "s":{ + "v":{ + "s":"11" + }, + "i":"c91ffb7c" + } + }, + { + "c":[ + { + "u":{ + "a":"Date", + "c":18, + "d":1693497600 + } + }, + { + "u":{ + "a":"Date", + "c":19, + "d":1693497600 + } + } + ], + "s":{ + "v":{ + "s":"12" + }, + "i":"c12182ef" + } + }, + { + "c":[ + { + "u":{ + "a":"Country", + "c":26, + "l":[ + "5a85699e7343a36d89ee75dca859f7a73cb6be89182095bffb021d1d78de046c" + ] + } + }, + { + "u":{ + "a":"Country", + "c":27, + "l":[ + "5a85699e7343a36d89ee75dca859f7a73cb6be89182095bffb021d1d78de046c" + ] + } + } + ], + "s":{ + "v":{ + "s":"13h" + }, + "i":"a16b1a17" + } + }, + { + "c":[ + { + "u":{ + "a":"Country", + "c":34, + "l":[ + "USA" + ] + } + }, + { + "u":{ + "a":"Country", + "c":35, + "l":[ + "USA" + ] + } + } + ], + "s":{ + "v":{ + "s":"13c" + }, + "i":"1a17d1b3" + } + } + ], + "v":{ + "s":"default" + }, + "i":"9ff25f81" + }, + "arrayContainsCaseCheckDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":26, + "l":[ + "00083f86e0f648b23f6721d43033bbef14378266fbf7de8a6760cc2ad237e9f3" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"5d80eff1" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"ce055a38" + }, + "arrayContainsDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":26, + "l":[ + "c2d5024661bc0e13f769cbb28bbfab7b78dac88b7876f007020f2e7cd47b1114" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"147fdd01" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"5f573f9c" + }, + "arrayDoesNotContainCaseCheckDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":27, + "l":[ + "14748140c64ecab48c7fd13f03811fe6390c8f578c99df96cf36fc2c6152f660" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"d4ad5730" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"df4915fd" + }, + "arrayDoesNotContainDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":27, + "l":[ + "bc0c24462c5098e434c63c7fcc6343af5000a4e7affd309f365edd4ccb7f428b" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"c2161ac9" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"41910880" + }, + "boolTrueIn202304":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":19, + "d":1680307200 + } + }, + { + "u":{ + "a":"Custom1", + "c":18, + "d":1682899200 + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"6948d7cd" + } + } + ], + "v":{ + "b":false + }, + "i":"ae2a09bd" + }, + "countryPercentageAttribute":{ + "t":1, + "a":"Country", + "p":[ + { + "p":50, + "v":{ + "s":"Falcon" + }, + "i":"2b05fd81" + }, + { + "p":50, + "v":{ + "s":"Horse" + }, + "i":"e28b6a82" + } + ], + "v":{ + "s":"Chicken" + }, + "i":"29bb6bbb" + }, + "customPercentageAttribute":{ + "t":1, + "a":"Custom1", + "p":[ + { + "p":50, + "v":{ + "s":"Falcon" + }, + "i":"3715712d" + }, + { + "p":50, + "v":{ + "s":"Horse" + }, + "i":"7b3542d5" + } + ], + "v":{ + "s":"Chicken" + }, + "i":"50466fb6" + }, + "missingPercentageAttribute":{ + "t":1, + "a":"NotFound", + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "14_902a42101e8b77851c98456b383fd959ed0f5aed5b919b4a623c8c756cf0c3ab" + ] + } + } + ], + "p":[ + { + "p":50, + "v":{ + "s":"Falcon" + }, + "i":"4b7d88ba" + }, + { + "p":50, + "v":{ + "s":"Horse" + }, + "i":"a1c2c9a9" + } + ] + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "14_902a42101e8b77851c98456b383fd959ed0f5aed5b919b4a623c8c756cf0c3ab" + ] + } + } + ], + "s":{ + "v":{ + "s":"NotFound" + }, + "i":"8aa042fe" + } + } + ], + "v":{ + "s":"Chicken" + }, + "i":"e5107172" + }, + "stringArrayContainsAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":34, + "l":[ + "read", + "write", + "execute" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"9ddb8a37" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"0d45ab4b" + }, + "stringArrayContainsAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":26, + "l":[ + "090415ce6b462a2152e06d68aeb7c452a564d19a262eb959c510636a189e105d", + "0aa50b49ca02a59cf507856d88ab25b76a7e69e553d25e67254359d8bbb8b1d6", + "1aa91982f7ccae1943f05ac437d635b3261e3ce06aba846dd2ecc6332e4675c2" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"aa03b1ff" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"203317f5" + }, + "stringArrayNotContainsAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":35, + "l":[ + "read", + "write", + "execute" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"15c865df" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"6df210da" + }, + "stringArrayNotContainsAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":27, + "l":[ + "b645a36d4d2e24a0612296ded074eace87ce10a0da21c5cb2ac9a6dccbe79cc5", + "552a92975423ea255384f48a6387be172c05ac72238f90050cdfe27cc4659cfd", + "4c00d4171202dbeb35830b1df8e47185968351c771bb2f633ea50ac1c049e016" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"259816ba" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"60b961b0" + }, + "stringContainsAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@verizon.com", + "@verizon.net", + "@aol.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"09af657f" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"063bcf39" + }, + "stringDoseNotEqualDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":21, + "s":"3cb496a1c3844215c784116ce9e91c143860d7ef18f7fadadcc901b8df5d235c" + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"8e423808" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"1835a09a" + }, + "stringEndsWithAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":32, + "l":[ + "@verizon.com", + "@verizon.net", + "@aol.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"33d35402" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"31976ec3" + }, + "stringEndsWithAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "12_781f5f9b054a14721a835fcdd2d03ac6d45d99eb55b387bf5938904c8f65aa35", + "12_d15f199cfbd96ef1c6f7683e66d5e0f85c9c591ce377289abb2e785fc71bbdf1", + "8_6ee234c513b7518bd705cc1049576c1e757a201200ff26a6a3a91821f954c6cb" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"7231ddf8" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"de17fd2a" + }, + "stringEndsWithDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "14_236ec291c9b54fad3373a0b7e0e465b33459198fd1027838c101516ad8ae1b39" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"d7a00741" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"45b7d922" + }, + "stringEqualsCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":28, + "s":"a@configcat.com" + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"087e01dd" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"89785ab3" + }, + "stringEqualsDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":20, + "s":"86e6b430939acac093f3d8c48be10798896f0abf3b96e2e39080acedd925d887" + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"703c31ed" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"adc0b01c" + }, + "stringNotContainsAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":3, + "l":[ + "@verizon.com", + "@verizon.net", + "@aol.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"49627b36" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"36848b03" + }, + "stringNotEndsWithAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":33, + "l":[ + "@verizon.com", + "@verizon.net", + "@aol.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"886ced9d" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"864b6202" + }, + "stringNotEndsWithAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":25, + "l":[ + "12_295e66dc483034349bc6cffd14190ccff949ac1f34df5fbf01437b8162719b98", + "12_11933417446186b92f63db5931a275f156485d0aef5ee8e265afb350921bd0b1", + "8_aa181c1d9462e3916cb05669e2c408541edf03dad19a9655b340eb32ddd4d060" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"6eb0ec3a" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"7020bcd6" + }, + "stringNotEndsWithDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":25, + "l":[ + "14_141d3f398885dd06d6c14e6629602125d8cb0dddf2ef85896f682c74abb4bc28" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"d37b6f18" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"91ba1bcb" + }, + "stringNotEqualsCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":29, + "s":"b@configcat.com" + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"3ace20fb" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"09c9725f" + }, + "stringNotStartsWithAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":31, + "l":[ + "u@", + "a@", + "b@" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"3717f2ca" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"1d661433" + }, + "stringNotStartsWithAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":23, + "l":[ + "2_b0ab1063bba4431f95710a8bd9c9e2bcd1cebc09fe6803cf87ee501d045e8ee0", + "2_9206a0a6b987410488c8020650788c597509af05aa5327f92cae1c999c401c34", + "2_d8f5c200e2c54b0f84f7df024576f799f484d0e258c6058142a4993daa5e2998" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"b5ba025e" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"c35929e3" + }, + "stringNotStartsWithDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":23, + "l":[ + "1_061a38ed8d5955d90b88d1b1949512a45032390a2581f3c7e36597f7459a48c7" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"72c4e1ac" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"2b16da78" + }, + "stringStartsWithAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":30, + "l":[ + "u@", + "a@", + "b@" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"9e55f5cf" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"e170a185" + }, + "stringStartsWithAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":22, + "l":[ + "2_52f47d1a193071a304e41812055cc1282d90287e5c993ae159fa08fb1ccd3656", + "2_6e5ae1bb586660088d330ac106bbf6b7f2b43be1ca70dbb766f203b76bc84049", + "2_3edba8b738135ad32e85d8695acb8d8511ac67d83f50da55abd9ba469da88efe" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"1d9b7603" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"dd5b3211" + }, + "stringStartsWithDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":22, + "l":[ + "1_55e0b0566d89dc0fda2323efcfb958c782a4648513bdcc4dc84b044fd34230dd" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"3b409872" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"3659b0fe" + } + } + } + """.trimIndent() + override val tests: Array = arrayOf( + TestCase( + key = "boolTrueIn202304", + defaultValue = true, + returnValue = false, + user = ConfigCatUser("12345", custom = mapOf("Custom1" to "2023.04.10")), + expectedLog = """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/src/commonTest/kotlin/com/configcat/evaluation/data/ListTruncationTests.kt b/src/commonTest/kotlin/com/configcat/evaluation/data/ListTruncationTests.kt new file mode 100644 index 00000000..5fbbe8a4 --- /dev/null +++ b/src/commonTest/kotlin/com/configcat/evaluation/data/ListTruncationTests.kt @@ -0,0 +1,65 @@ +package com.configcat.evaluation.data + +import com.configcat.ConfigCatUser + +object ListTruncationTests : TestSet { + override val sdkKey = "configcat-sdk-test-key/0000000000000000000001" + override val baseUrl = null + override val jsonOverride = """{ + "p": { + "u": "https://cdn-global.configcat.com", + "r": 0, + "s": "W8tBvwwMoeP6Ht74jMCI7aPNTc\u002B1W6rtwob18ojXQ9U=" + }, + "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 } } + } + ] + } + } +} +""" + override val tests: Array = arrayOf( + TestCase( + key = "booleanKey1", + defaultValue = false, + returnValue = true, + expectedLog = """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'.""", + user = ConfigCatUser("12") + ) + ) +} diff --git a/src/commonTest/kotlin/com/configcat/evaluation/data/NumberValidationTests.kt b/src/commonTest/kotlin/com/configcat/evaluation/data/NumberValidationTests.kt new file mode 100644 index 00000000..b24a9be4 --- /dev/null +++ b/src/commonTest/kotlin/com/configcat/evaluation/data/NumberValidationTests.kt @@ -0,0 +1,185 @@ +package com.configcat.evaluation.data + +import com.configcat.ConfigCatUser + +object NumberValidationTests : TestSet { + override val sdkKey = "PKDVCLf-Hq-h-kCzMp-L7Q/uGyK3q9_ckmdxRyI7vjwCw" + override val baseUrl = null + override val jsonOverride = """ + { + "p":{ + "u":"https://cdn-global.configcat.com", + "r":0, + "s":"ZGhr6z5Q/fidMVebU3WWd0DnMBvNl7NT6QdM/wUqt3U=" + }, + "f":{ + "number":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":11, + "d":5 + } + } + ], + "s":{ + "v":{ + "s":"\u003C\u003E5" + }, + "i":"a41938c5" + } + } + ], + "v":{ + "s":"Default" + }, + "i":"5ced27a9" + }, + "numberWithPercentage":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":12, + "d":2.1 + } + } + ], + "s":{ + "v":{ + "s":"\u003C2.1" + }, + "i":"a900bc23" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":13, + "d":2.1 + } + } + ], + "s":{ + "v":{ + "s":"\u003C=2,1" + }, + "i":"2c85f73d" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":10, + "d":3.5 + } + } + ], + "s":{ + "v":{ + "s":"=3.5" + }, + "i":"ae86baf5" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":14, + "d":5 + } + } + ], + "s":{ + "v":{ + "s":"\u003E5" + }, + "i":"c6924001" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":15, + "d":5 + } + } + ], + "s":{ + "v":{ + "s":"\u003E=5" + }, + "i":"8090543a" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":11, + "d":4.2 + } + } + ], + "s":{ + "v":{ + "s":"\u003C\u003E4.2" + }, + "i":"2691fade" + } + } + ], + "p":[ + { + "p":80, + "v":{ + "s":"80%" + }, + "i":"ad5f05a7" + }, + { + "p":20, + "v":{ + "s":"20%" + }, + "i":"786b696f" + } + ], + "v":{ + "s":"Default" + }, + "i":"642bbb26" + } + } +}""" + override val tests: Array = arrayOf( + TestCase( + key = "number", + defaultValue = "default", + returnValue = "Default", + user = ConfigCatUser("12345", custom = mapOf("Custom1" to "not_a_number")), + expectedLog = """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/src/commonTest/kotlin/com/configcat/evaluation/data/OneTargetingRuleTests.kt b/src/commonTest/kotlin/com/configcat/evaluation/data/OneTargetingRuleTests.kt new file mode 100644 index 00000000..59a24215 --- /dev/null +++ b/src/commonTest/kotlin/com/configcat/evaluation/data/OneTargetingRuleTests.kt @@ -0,0 +1,648 @@ +package com.configcat.evaluation.data + +import com.configcat.ConfigCatUser + +object OneTargetingRuleTests : TestSet { + override val sdkKey = "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A" + override val baseUrl = null + override val jsonOverride = """ + { + "p":{ + "u":"https://cdn-global.configcat.com", + "r":0, + "s":"pkw2BWOIXiTrXO53/OPECHP9OeJzmW8y/yV47\u002BQ8HLM=" + }, + "f":{ + "bool30TrueAdvancedRules":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":0, + "l":[ + "a@configcat.com", + "b@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "b":false + }, + "i":"385d9803" + } + }, + { + "c":[ + { + "u":{ + "a":"Country", + "c":2, + "l":[ + "United" + ] + } + } + ], + "s":{ + "v":{ + "b":false + }, + "i":"385d9803" + } + } + ], + "p":[ + { + "p":30, + "v":{ + "b":true + }, + "i":"607147d5" + }, + { + "p":70, + "v":{ + "b":false + }, + "i":"385d9803" + } + ], + "v":{ + "b":true + }, + "i":"607147d5" + }, + "boolDefaultFalse":{ + "t":0, + "v":{ + "b":false + }, + "i":"489a16d2" + }, + "boolDefaultTrue":{ + "t":0, + "v":{ + "b":true + }, + "i":"09513143" + }, + "double25Pi25E25Gr25Zero":{ + "t":3, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "d":5.561 + }, + "i":"3f7826de" + } + } + ], + "p":[ + { + "p":25, + "v":{ + "d":3.1415 + }, + "i":"6d75b4d3" + }, + { + "p":25, + "v":{ + "d":2.7182 + }, + "i":"183ee713" + }, + { + "p":25, + "v":{ + "d":1.61803 + }, + "i":"01eb6326" + }, + { + "p":25, + "v":{ + "d":0 + }, + "i":"64c434ff" + } + ], + "v":{ + "d":-1 + }, + "i":"9503a1de" + }, + "doubleDefaultPi":{ + "t":3, + "v":{ + "d":3.1415 + }, + "i":"5af8acc7" + }, + "integer25One25Two25Three25FourAdvancedRules":{ + "t":2, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "i":5 + }, + "i":"58136ba2" + } + } + ], + "p":[ + { + "p":25, + "v":{ + "i":1 + }, + "i":"11634414" + }, + { + "p":25, + "v":{ + "i":2 + }, + "i":"5530655d" + }, + { + "p":25, + "v":{ + "i":3 + }, + "i":"2ad19a52" + }, + { + "p":25, + "v":{ + "i":4 + }, + "i":"41b30851" + } + ], + "v":{ + "i":-1 + }, + "i":"ce3c4f5a" + }, + "integerDefaultOne":{ + "t":2, + "v":{ + "i":1 + }, + "i":"faadbf54" + }, + "keySampleText":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Country", + "c":0, + "l":[ + "Hungary", + "Bahamas" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"9fa0e57e" + } + }, + { + "c":[ + { + "u":{ + "a":"SubscriptionType", + "c":0, + "l":[ + "unlimited" + ] + } + } + ], + "s":{ + "v":{ + "s":"Lion" + }, + "i":"2be6b03f" + } + } + ], + "p":[ + { + "p":50, + "v":{ + "s":"Falcon" + }, + "i":"baff2362" + }, + { + "p":50, + "v":{ + "s":"Horse" + }, + "i":"dab78ba5" + } + ], + "v":{ + "s":"Cat" + }, + "i":"69ef126c" + }, + "string25Cat25Dog25Falcon25Horse":{ + "t":1, + "p":[ + { + "p":25, + "v":{ + "s":"Cat" + }, + "i":"d227b334" + }, + { + "p":25, + "v":{ + "s":"Dog" + }, + "i":"622f5d07" + }, + { + "p":25, + "v":{ + "s":"Falcon" + }, + "i":"0ff32bab" + }, + { + "p":25, + "v":{ + "s":"Horse" + }, + "i":"6c597441" + } + ], + "v":{ + "s":"Chicken" + }, + "i":"2588a3e6" + }, + "string25Cat25Dog25Falcon25HorseAdvancedRules":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Country", + "c":0, + "l":[ + "Hungary", + "United Kingdom" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dolphin" + }, + "i":"3accb1d0" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":2, + "l":[ + "admi" + ] + } + } + ], + "s":{ + "v":{ + "s":"Lion" + }, + "i":"e95ebf10" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Kitten" + }, + "i":"88243650" + } + } + ], + "p":[ + { + "p":25, + "v":{ + "s":"Cat" + }, + "i":"83461b47" + }, + { + "p":25, + "v":{ + "s":"Dog" + }, + "i":"4f026fbc" + }, + { + "p":25, + "v":{ + "s":"Falcon" + }, + "i":"392a4d59" + }, + { + "p":25, + "v":{ + "s":"Horse" + }, + "i":"bb66b1f3" + } + ], + "v":{ + "s":"Chicken" + }, + "i":"8250ef5a" + }, + "string75Cat0Dog25Falcon0Horse":{ + "t":1, + "p":[ + { + "p":75, + "v":{ + "s":"Cat" + }, + "i":"93f5a1c0" + }, + { + "p":0, + "v":{ + "s":"Dog" + }, + "i":"b8f49554" + }, + { + "p":25, + "v":{ + "s":"Falcon" + }, + "i":"7beaf504" + }, + { + "p":0, + "v":{ + "s":"Horse" + }, + "i":"30ee31af" + } + ], + "v":{ + "s":"Chicken" + }, + "i":"aa65b5ce" + }, + "stringContainsDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"d0cd8f06" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"ce564c3a" + }, + "stringDefaultCat":{ + "t":1, + "v":{ + "s":"Cat" + }, + "i":"7a0be518" + }, + "stringIsInDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":0, + "l":[ + "a@configcat.com", + "b@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"5b64d9b4" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":0, + "l":[ + "admin" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"5b64d9b4" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"83372510" + }, + "stringIsNotInDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":1, + "l":[ + "a@configcat.com", + "b@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"6ada5ff2" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"2459598d" + }, + "stringNotContainsDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":3, + "l":[ + "@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"f7f8f43d" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"44ab483a" + } + } + } + """.trimIndent() + override val tests: Array = arrayOf( + TestCase( + key = "stringContainsDogDefaultCat", + defaultValue = "default", + returnValue = "Cat", + expectedLog = """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'.""", + user = null + ), + TestCase( + key = "stringContainsDogDefaultCat", + defaultValue = "default", + returnValue = "Cat", + user = ConfigCatUser("12345"), + expectedLog = """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'.""" + ), + TestCase( + key = "stringContainsDogDefaultCat", + defaultValue = "default", + returnValue = "Cat", + user = ConfigCatUser("12345", "joe@example.com"), + expectedLog = """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'.""" + ), + TestCase( + key = "stringContainsDogDefaultCat", + defaultValue = "default", + returnValue = "Dog", + user = ConfigCatUser("12345", "joe@configcat.com"), + expectedLog = """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/src/commonTest/kotlin/com/configcat/evaluation/data/OptionsAfterTargetingRuleTests.kt b/src/commonTest/kotlin/com/configcat/evaluation/data/OptionsAfterTargetingRuleTests.kt new file mode 100644 index 00000000..1dc3facb --- /dev/null +++ b/src/commonTest/kotlin/com/configcat/evaluation/data/OptionsAfterTargetingRuleTests.kt @@ -0,0 +1,654 @@ +package com.configcat.evaluation.data + +import com.configcat.ConfigCatUser + +object OptionsAfterTargetingRuleTests : TestSet { + override val sdkKey = "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A" + override val baseUrl = null + override val jsonOverride = """ + { + "p":{ + "u":"https://cdn-global.configcat.com", + "r":0, + "s":"pkw2BWOIXiTrXO53/OPECHP9OeJzmW8y/yV47\u002BQ8HLM=" + }, + "f":{ + "bool30TrueAdvancedRules":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":0, + "l":[ + "a@configcat.com", + "b@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "b":false + }, + "i":"385d9803" + } + }, + { + "c":[ + { + "u":{ + "a":"Country", + "c":2, + "l":[ + "United" + ] + } + } + ], + "s":{ + "v":{ + "b":false + }, + "i":"385d9803" + } + } + ], + "p":[ + { + "p":30, + "v":{ + "b":true + }, + "i":"607147d5" + }, + { + "p":70, + "v":{ + "b":false + }, + "i":"385d9803" + } + ], + "v":{ + "b":true + }, + "i":"607147d5" + }, + "boolDefaultFalse":{ + "t":0, + "v":{ + "b":false + }, + "i":"489a16d2" + }, + "boolDefaultTrue":{ + "t":0, + "v":{ + "b":true + }, + "i":"09513143" + }, + "double25Pi25E25Gr25Zero":{ + "t":3, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "d":5.561 + }, + "i":"3f7826de" + } + } + ], + "p":[ + { + "p":25, + "v":{ + "d":3.1415 + }, + "i":"6d75b4d3" + }, + { + "p":25, + "v":{ + "d":2.7182 + }, + "i":"183ee713" + }, + { + "p":25, + "v":{ + "d":1.61803 + }, + "i":"01eb6326" + }, + { + "p":25, + "v":{ + "d":0 + }, + "i":"64c434ff" + } + ], + "v":{ + "d":-1 + }, + "i":"9503a1de" + }, + "doubleDefaultPi":{ + "t":3, + "v":{ + "d":3.1415 + }, + "i":"5af8acc7" + }, + "integer25One25Two25Three25FourAdvancedRules":{ + "t":2, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "i":5 + }, + "i":"58136ba2" + } + } + ], + "p":[ + { + "p":25, + "v":{ + "i":1 + }, + "i":"11634414" + }, + { + "p":25, + "v":{ + "i":2 + }, + "i":"5530655d" + }, + { + "p":25, + "v":{ + "i":3 + }, + "i":"2ad19a52" + }, + { + "p":25, + "v":{ + "i":4 + }, + "i":"41b30851" + } + ], + "v":{ + "i":-1 + }, + "i":"ce3c4f5a" + }, + "integerDefaultOne":{ + "t":2, + "v":{ + "i":1 + }, + "i":"faadbf54" + }, + "keySampleText":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Country", + "c":0, + "l":[ + "Hungary", + "Bahamas" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"9fa0e57e" + } + }, + { + "c":[ + { + "u":{ + "a":"SubscriptionType", + "c":0, + "l":[ + "unlimited" + ] + } + } + ], + "s":{ + "v":{ + "s":"Lion" + }, + "i":"2be6b03f" + } + } + ], + "p":[ + { + "p":50, + "v":{ + "s":"Falcon" + }, + "i":"baff2362" + }, + { + "p":50, + "v":{ + "s":"Horse" + }, + "i":"dab78ba5" + } + ], + "v":{ + "s":"Cat" + }, + "i":"69ef126c" + }, + "string25Cat25Dog25Falcon25Horse":{ + "t":1, + "p":[ + { + "p":25, + "v":{ + "s":"Cat" + }, + "i":"d227b334" + }, + { + "p":25, + "v":{ + "s":"Dog" + }, + "i":"622f5d07" + }, + { + "p":25, + "v":{ + "s":"Falcon" + }, + "i":"0ff32bab" + }, + { + "p":25, + "v":{ + "s":"Horse" + }, + "i":"6c597441" + } + ], + "v":{ + "s":"Chicken" + }, + "i":"2588a3e6" + }, + "string25Cat25Dog25Falcon25HorseAdvancedRules":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Country", + "c":0, + "l":[ + "Hungary", + "United Kingdom" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dolphin" + }, + "i":"3accb1d0" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":2, + "l":[ + "admi" + ] + } + } + ], + "s":{ + "v":{ + "s":"Lion" + }, + "i":"e95ebf10" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Kitten" + }, + "i":"88243650" + } + } + ], + "p":[ + { + "p":25, + "v":{ + "s":"Cat" + }, + "i":"83461b47" + }, + { + "p":25, + "v":{ + "s":"Dog" + }, + "i":"4f026fbc" + }, + { + "p":25, + "v":{ + "s":"Falcon" + }, + "i":"392a4d59" + }, + { + "p":25, + "v":{ + "s":"Horse" + }, + "i":"bb66b1f3" + } + ], + "v":{ + "s":"Chicken" + }, + "i":"8250ef5a" + }, + "string75Cat0Dog25Falcon0Horse":{ + "t":1, + "p":[ + { + "p":75, + "v":{ + "s":"Cat" + }, + "i":"93f5a1c0" + }, + { + "p":0, + "v":{ + "s":"Dog" + }, + "i":"b8f49554" + }, + { + "p":25, + "v":{ + "s":"Falcon" + }, + "i":"7beaf504" + }, + { + "p":0, + "v":{ + "s":"Horse" + }, + "i":"30ee31af" + } + ], + "v":{ + "s":"Chicken" + }, + "i":"aa65b5ce" + }, + "stringContainsDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"d0cd8f06" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"ce564c3a" + }, + "stringDefaultCat":{ + "t":1, + "v":{ + "s":"Cat" + }, + "i":"7a0be518" + }, + "stringIsInDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":0, + "l":[ + "a@configcat.com", + "b@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"5b64d9b4" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":0, + "l":[ + "admin" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"5b64d9b4" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"83372510" + }, + "stringIsNotInDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":1, + "l":[ + "a@configcat.com", + "b@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"6ada5ff2" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"2459598d" + }, + "stringNotContainsDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":3, + "l":[ + "@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"f7f8f43d" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"44ab483a" + } + } +}""" + override val tests: Array = arrayOf( + TestCase( + key = "integer25One25Two25Three25FourAdvancedRules", + defaultValue = 42, + returnValue = -1, + user = null, + expectedLog = """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'.""" + ), + TestCase( + key = "integer25One25Two25Three25FourAdvancedRules", + defaultValue = 42, + returnValue = 2, + user = ConfigCatUser("12345"), + expectedLog = """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'.""" + ), + TestCase( + key = "integer25One25Two25Three25FourAdvancedRules", + defaultValue = 42, + returnValue = 2, + user = ConfigCatUser("12345", "joe@example.com"), + expectedLog = """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'.""" + ), + TestCase( + key = "integer25One25Two25Three25FourAdvancedRules", + defaultValue = 42, + returnValue = 5, + user = ConfigCatUser("12345", "joe@configcat.com"), + expectedLog = """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/src/commonTest/kotlin/com/configcat/evaluation/data/OptionsBasedOnCustomAttrTests.kt b/src/commonTest/kotlin/com/configcat/evaluation/data/OptionsBasedOnCustomAttrTests.kt new file mode 100644 index 00000000..ff565c65 --- /dev/null +++ b/src/commonTest/kotlin/com/configcat/evaluation/data/OptionsBasedOnCustomAttrTests.kt @@ -0,0 +1,205 @@ +package com.configcat.evaluation.data + +import com.configcat.ConfigCatUser + +object OptionsBasedOnCustomAttrTests : TestSet { + override val sdkKey = "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw" + override val baseUrl = null + override val jsonOverride = """ + { + "p":{ + "u":"https://cdn-global.configcat.com", + "r":0, + "s":"3LIHe8O2rOLIWDXI8AeWiS3eKLpk/qBudXSP2ki6AgM=" + }, + "f":{ + "dependentFlag":{ + "t":1, + "r":[ + { + "c":[ + { + "p":{ + "f":"key1", + "c":0, + "v":{ + "s":"value1" + } + } + } + ], + "s":{ + "v":{ + "s":"Chicken" + }, + "i":"5916066a" + } + }, + { + "c":[ + { + "p":{ + "f":"key1", + "c":0, + "v":{ + "s":"value1" + } + } + }, + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "12_631017d1fa355bce15ab9889e8b75223db972383dbd9b5fed9791c6fbb5c00c0" + ] + } + } + ], + "s":{ + "v":{ + "s":"Cat" + }, + "i":"a4346a91" + } + } + ], + "v":{ + "s":"dependentFlag" + }, + "i":"e0afe4ca" + }, + "key1":{ + "t":1, + "v":{ + "s":"value1" + }, + "i":"1605ae93" + }, + "string75Cat0Dog25Falcon0HorseCustomAttr":{ + "t":1, + "a":"Country", + "p":[ + { + "p":75, + "v":{ + "s":"Cat" + }, + "i":"8285ed60" + }, + { + "p":0, + "v":{ + "s":"Dog" + }, + "i":"597e1dd1" + }, + { + "p":25, + "v":{ + "s":"Falcon" + }, + "i":"8896564a" + }, + { + "p":0, + "v":{ + "s":"Horse" + }, + "i":"d1944e2c" + } + ], + "v":{ + "s":"Chicken" + }, + "i":"13ad5bbc" + }, + "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat":{ + "t":1, + "a":"Country", + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@configcat.com" + ] + } + } + ], + "p":[ + { + "p":75, + "v":{ + "s":"Cat" + }, + "i":"05a1d8f3" + }, + { + "p":0, + "v":{ + "s":"Dog" + }, + "i":"52a42c84" + }, + { + "p":25, + "v":{ + "s":"Falcon" + }, + "i":"06c2db91" + }, + { + "p":0, + "v":{ + "s":"Horse" + }, + "i":"fe226091" + } + ] + } + ], + "v":{ + "s":"Cat" + }, + "i":"05a1d8f3" + } + } +}""" + override val tests: Array = arrayOf( + TestCase( + key = "string75Cat0Dog25Falcon0HorseCustomAttr", + defaultValue = "default", + returnValue = "Chicken", + user = null, + expectedLog = """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'.""" + ), + TestCase( + key = "string75Cat0Dog25Falcon0HorseCustomAttr", + defaultValue = "default", + returnValue = "Chicken", + user = ConfigCatUser("12345"), + expectedLog = """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'.""" + ), + TestCase( + key = "string75Cat0Dog25Falcon0HorseCustomAttr", + defaultValue = "default", + returnValue = "Cat", + user = ConfigCatUser("12345", country = "US"), + expectedLog = """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/src/commonTest/kotlin/com/configcat/evaluation/data/OptionsBasedOnUserIdTests.kt b/src/commonTest/kotlin/com/configcat/evaluation/data/OptionsBasedOnUserIdTests.kt new file mode 100644 index 00000000..e7f94bc6 --- /dev/null +++ b/src/commonTest/kotlin/com/configcat/evaluation/data/OptionsBasedOnUserIdTests.kt @@ -0,0 +1,625 @@ +package com.configcat.evaluation.data + +import com.configcat.ConfigCatUser + +object OptionsBasedOnUserIdTests : TestSet { + override val sdkKey = "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A" + override val baseUrl = null + override val jsonOverride = """ + { + "p":{ + "u":"https://cdn-global.configcat.com", + "r":0, + "s":"pkw2BWOIXiTrXO53/OPECHP9OeJzmW8y/yV47\u002BQ8HLM=" + }, + "f":{ + "bool30TrueAdvancedRules":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":0, + "l":[ + "a@configcat.com", + "b@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "b":false + }, + "i":"385d9803" + } + }, + { + "c":[ + { + "u":{ + "a":"Country", + "c":2, + "l":[ + "United" + ] + } + } + ], + "s":{ + "v":{ + "b":false + }, + "i":"385d9803" + } + } + ], + "p":[ + { + "p":30, + "v":{ + "b":true + }, + "i":"607147d5" + }, + { + "p":70, + "v":{ + "b":false + }, + "i":"385d9803" + } + ], + "v":{ + "b":true + }, + "i":"607147d5" + }, + "boolDefaultFalse":{ + "t":0, + "v":{ + "b":false + }, + "i":"489a16d2" + }, + "boolDefaultTrue":{ + "t":0, + "v":{ + "b":true + }, + "i":"09513143" + }, + "double25Pi25E25Gr25Zero":{ + "t":3, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "d":5.561 + }, + "i":"3f7826de" + } + } + ], + "p":[ + { + "p":25, + "v":{ + "d":3.1415 + }, + "i":"6d75b4d3" + }, + { + "p":25, + "v":{ + "d":2.7182 + }, + "i":"183ee713" + }, + { + "p":25, + "v":{ + "d":1.61803 + }, + "i":"01eb6326" + }, + { + "p":25, + "v":{ + "d":0 + }, + "i":"64c434ff" + } + ], + "v":{ + "d":-1 + }, + "i":"9503a1de" + }, + "doubleDefaultPi":{ + "t":3, + "v":{ + "d":3.1415 + }, + "i":"5af8acc7" + }, + "integer25One25Two25Three25FourAdvancedRules":{ + "t":2, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "i":5 + }, + "i":"58136ba2" + } + } + ], + "p":[ + { + "p":25, + "v":{ + "i":1 + }, + "i":"11634414" + }, + { + "p":25, + "v":{ + "i":2 + }, + "i":"5530655d" + }, + { + "p":25, + "v":{ + "i":3 + }, + "i":"2ad19a52" + }, + { + "p":25, + "v":{ + "i":4 + }, + "i":"41b30851" + } + ], + "v":{ + "i":-1 + }, + "i":"ce3c4f5a" + }, + "integerDefaultOne":{ + "t":2, + "v":{ + "i":1 + }, + "i":"faadbf54" + }, + "keySampleText":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Country", + "c":0, + "l":[ + "Hungary", + "Bahamas" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"9fa0e57e" + } + }, + { + "c":[ + { + "u":{ + "a":"SubscriptionType", + "c":0, + "l":[ + "unlimited" + ] + } + } + ], + "s":{ + "v":{ + "s":"Lion" + }, + "i":"2be6b03f" + } + } + ], + "p":[ + { + "p":50, + "v":{ + "s":"Falcon" + }, + "i":"baff2362" + }, + { + "p":50, + "v":{ + "s":"Horse" + }, + "i":"dab78ba5" + } + ], + "v":{ + "s":"Cat" + }, + "i":"69ef126c" + }, + "string25Cat25Dog25Falcon25Horse":{ + "t":1, + "p":[ + { + "p":25, + "v":{ + "s":"Cat" + }, + "i":"d227b334" + }, + { + "p":25, + "v":{ + "s":"Dog" + }, + "i":"622f5d07" + }, + { + "p":25, + "v":{ + "s":"Falcon" + }, + "i":"0ff32bab" + }, + { + "p":25, + "v":{ + "s":"Horse" + }, + "i":"6c597441" + } + ], + "v":{ + "s":"Chicken" + }, + "i":"2588a3e6" + }, + "string25Cat25Dog25Falcon25HorseAdvancedRules":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Country", + "c":0, + "l":[ + "Hungary", + "United Kingdom" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dolphin" + }, + "i":"3accb1d0" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":2, + "l":[ + "admi" + ] + } + } + ], + "s":{ + "v":{ + "s":"Lion" + }, + "i":"e95ebf10" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Kitten" + }, + "i":"88243650" + } + } + ], + "p":[ + { + "p":25, + "v":{ + "s":"Cat" + }, + "i":"83461b47" + }, + { + "p":25, + "v":{ + "s":"Dog" + }, + "i":"4f026fbc" + }, + { + "p":25, + "v":{ + "s":"Falcon" + }, + "i":"392a4d59" + }, + { + "p":25, + "v":{ + "s":"Horse" + }, + "i":"bb66b1f3" + } + ], + "v":{ + "s":"Chicken" + }, + "i":"8250ef5a" + }, + "string75Cat0Dog25Falcon0Horse":{ + "t":1, + "p":[ + { + "p":75, + "v":{ + "s":"Cat" + }, + "i":"93f5a1c0" + }, + { + "p":0, + "v":{ + "s":"Dog" + }, + "i":"b8f49554" + }, + { + "p":25, + "v":{ + "s":"Falcon" + }, + "i":"7beaf504" + }, + { + "p":0, + "v":{ + "s":"Horse" + }, + "i":"30ee31af" + } + ], + "v":{ + "s":"Chicken" + }, + "i":"aa65b5ce" + }, + "stringContainsDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"d0cd8f06" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"ce564c3a" + }, + "stringDefaultCat":{ + "t":1, + "v":{ + "s":"Cat" + }, + "i":"7a0be518" + }, + "stringIsInDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":0, + "l":[ + "a@configcat.com", + "b@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"5b64d9b4" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":0, + "l":[ + "admin" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"5b64d9b4" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"83372510" + }, + "stringIsNotInDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":1, + "l":[ + "a@configcat.com", + "b@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"6ada5ff2" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"2459598d" + }, + "stringNotContainsDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":3, + "l":[ + "@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"f7f8f43d" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"44ab483a" + } + } +} + """.trimIndent() + override val tests: Array = arrayOf( + TestCase( + key = "string75Cat0Dog25Falcon0Horse", + defaultValue = "default", + returnValue = "Chicken", + user = null, + expectedLog = """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'.""" + ), + TestCase( + key = "string75Cat0Dog25Falcon0Horse", + defaultValue = "default", + returnValue = "Cat", + user = ConfigCatUser("12345"), + expectedLog = """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/src/commonTest/kotlin/com/configcat/evaluation/data/OptionsWithinTargetingRuleTests.kt b/src/commonTest/kotlin/com/configcat/evaluation/data/OptionsWithinTargetingRuleTests.kt new file mode 100644 index 00000000..09ed6a55 --- /dev/null +++ b/src/commonTest/kotlin/com/configcat/evaluation/data/OptionsWithinTargetingRuleTests.kt @@ -0,0 +1,234 @@ +package com.configcat.evaluation.data + +import com.configcat.ConfigCatUser + +object OptionsWithinTargetingRuleTests : TestSet { + override val sdkKey = "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw" + override val baseUrl = null + override val jsonOverride = """ + { + "p":{ + "u":"https://cdn-global.configcat.com", + "r":0, + "s":"3LIHe8O2rOLIWDXI8AeWiS3eKLpk/qBudXSP2ki6AgM=" + }, + "f":{ + "dependentFlag":{ + "t":1, + "r":[ + { + "c":[ + { + "p":{ + "f":"key1", + "c":0, + "v":{ + "s":"value1" + } + } + } + ], + "s":{ + "v":{ + "s":"Chicken" + }, + "i":"5916066a" + } + }, + { + "c":[ + { + "p":{ + "f":"key1", + "c":0, + "v":{ + "s":"value1" + } + } + }, + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "12_631017d1fa355bce15ab9889e8b75223db972383dbd9b5fed9791c6fbb5c00c0" + ] + } + } + ], + "s":{ + "v":{ + "s":"Cat" + }, + "i":"a4346a91" + } + } + ], + "v":{ + "s":"dependentFlag" + }, + "i":"e0afe4ca" + }, + "key1":{ + "t":1, + "v":{ + "s":"value1" + }, + "i":"1605ae93" + }, + "string75Cat0Dog25Falcon0HorseCustomAttr":{ + "t":1, + "a":"Country", + "p":[ + { + "p":75, + "v":{ + "s":"Cat" + }, + "i":"8285ed60" + }, + { + "p":0, + "v":{ + "s":"Dog" + }, + "i":"597e1dd1" + }, + { + "p":25, + "v":{ + "s":"Falcon" + }, + "i":"8896564a" + }, + { + "p":0, + "v":{ + "s":"Horse" + }, + "i":"d1944e2c" + } + ], + "v":{ + "s":"Chicken" + }, + "i":"13ad5bbc" + }, + "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat":{ + "t":1, + "a":"Country", + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@configcat.com" + ] + } + } + ], + "p":[ + { + "p":75, + "v":{ + "s":"Cat" + }, + "i":"05a1d8f3" + }, + { + "p":0, + "v":{ + "s":"Dog" + }, + "i":"52a42c84" + }, + { + "p":25, + "v":{ + "s":"Falcon" + }, + "i":"06c2db91" + }, + { + "p":0, + "v":{ + "s":"Horse" + }, + "i":"fe226091" + } + ] + } + ], + "v":{ + "s":"Cat" + }, + "i":"05a1d8f3" + } + } +}""" + override val tests: Array = arrayOf( + TestCase( + key = "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat", + defaultValue = "default", + returnValue = "Cat", + user = null, + expectedLog = """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'.""" + ), + TestCase( + key = "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat", + defaultValue = "default", + returnValue = "Cat", + user = ConfigCatUser("12345"), + expectedLog = """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'.""" + ), + TestCase( + key = "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat", + defaultValue = "default", + returnValue = "Cat", + user = ConfigCatUser("12345", "joe@example.com"), + expectedLog = """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'.""" + ), + TestCase( + key = "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat", + defaultValue = "default", + returnValue = "Cat", + user = ConfigCatUser("12345", "joe@configcat.com"), + expectedLog = """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'.""" + ), + TestCase( + key = "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat", + defaultValue = "default", + returnValue = "Cat", + user = ConfigCatUser("12345", "joe@configcat.com", "US"), + expectedLog = """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/src/commonTest/kotlin/com/configcat/evaluation/data/PrerequisiteFlagTests.kt b/src/commonTest/kotlin/com/configcat/evaluation/data/PrerequisiteFlagTests.kt new file mode 100644 index 00000000..54fd4d17 --- /dev/null +++ b/src/commonTest/kotlin/com/configcat/evaluation/data/PrerequisiteFlagTests.kt @@ -0,0 +1,660 @@ +package com.configcat.evaluation.data + +import com.configcat.ConfigCatUser + +object PrerequisiteFlagTests : TestSet { + override val sdkKey = "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/ByMO9yZNn02kXcm72lnY1A" + override val baseUrl = null + override val jsonOverride = """ + { + "p":{ + "u":"https://cdn-global.configcat.com", + "r":0, + "s":"9QUCCinAuxoxzwqYysygixlMORhwuxu3wg00Cs0mw5I=" + }, + "s":[ + { + "n":"Beta Users", + "r":[ + { + "a":"Email", + "c":16, + "l":[ + "edbdeb7620729047fb22ffab8ab349a7eac8d0aa473d3d47630182f89821541b", + "1960614f092689190de846b8c28fd52dbd9d70fe75cbcd28fb8ebcb616d9c525" + ] + } + ] + }, + { + "n":"Developers", + "r":[ + { + "a":"Email", + "c":16, + "l":[ + "1fc2737a1be8ace9e5e6fa78b87b304374ce85b3a94202844834472cd779a73f", + "f729137a8dfed4b2f616c4213f9555150a0dba6f75736e77deb539f1806f5077" + ] + } + ] + } + ], + "f":{ + "dependentFeature":{ + "t":1, + "r":[ + { + "c":[ + { + "p":{ + "f":"mainFeature", + "c":0, + "v":{ + "s":"target" + } + } + } + ], + "p":[ + { + "p":25, + "v":{ + "s":"Cat" + }, + "i":"993d7ee0" + }, + { + "p":25, + "v":{ + "s":"Dog" + }, + "i":"08b8348e" + }, + { + "p":25, + "v":{ + "s":"Falcon" + }, + "i":"a6fb7a01" + }, + { + "p":25, + "v":{ + "s":"Horse" + }, + "i":"699fb4bf" + } + ] + } + ], + "v":{ + "s":"Chicken" + }, + "i":"e6198f92" + }, + "dependentFeatureMultipleLevels":{ + "t":1, + "r":[ + { + "c":[ + { + "p":{ + "f":"intermediateFeature", + "c":0, + "v":{ + "b":true + } + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"cdbc4728" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"1c895d65" + }, + "dependentFeatureWithUserCondition":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":16, + "l":[ + "c331a2f2bea132f0e39651cb8834b420a189bbf03a3985ec05daf78c7ca4baf7", + "4916de795d199f76ad7d056d5d14311d71841cf6e6dd64b863be396ff590788c" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"ef802e43" + } + }, + { + "c":[ + { + "p":{ + "f":"mainFeatureWithoutUserCondition", + "c":0, + "v":{ + "b":true + } + } + } + ], + "p":[ + { + "p":34, + "v":{ + "s":"Cat" + }, + "i":"4a65d6ef" + }, + { + "p":33, + "v":{ + "s":"Horse" + }, + "i":"fc3bb22b" + }, + { + "p":33, + "v":{ + "s":"Falcon" + }, + "i":"32e0e525" + } + ] + } + ], + "v":{ + "s":"Chicken" + }, + "i":"472e10f4" + }, + "dependentFeatureWithUserCondition2":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":16, + "l":[ + "c9a31439dc0f6651b26e19af7618bde322667d740b7420f7a4205b538a642b8a", + "10743927669a473a7b7871e4986e38f632721b5f590bf707a3824350392f9925" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"78eceed0" + } + }, + { + "c":[ + { + "p":{ + "f":"mainFeature", + "c":0, + "v":{ + "s":"public" + } + } + } + ], + "p":[ + { + "p":34, + "v":{ + "s":"Cat" + }, + "i":"72b97d0e" + }, + { + "p":33, + "v":{ + "s":"Horse" + }, + "i":"81846c69" + }, + { + "p":33, + "v":{ + "s":"Falcon" + }, + "i":"e2f3b509" + } + ] + }, + { + "c":[ + { + "p":{ + "f":"mainFeature", + "c":0, + "v":{ + "s":"public" + } + } + } + ], + "s":{ + "v":{ + "s":"Frog" + }, + "i":"cfd56c79" + } + } + ], + "v":{ + "s":"Chicken" + }, + "i":"9e8d62c6" + }, + "emailAnd":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":22, + "l":[ + "4_5ef657b8b86d0915729e13ed68a58dfe62927698c33fc0fed7fd27c1ce07083b" + ] + } + }, + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@" + ] + } + }, + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "20_a64e9a2f841a01b6e97458b09e67931a0d749b4de1d7a287edafcc4ca67a574c" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"a1393561" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"bdabd589" + }, + "emailOr":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":22, + "l":[ + "5_831d3723899ca121aecd407492308e20293440167e867e209ffbed9394fedc7e" + ] + } + } + ], + "s":{ + "v":{ + "s":"Jane" + }, + "i":"01383bbf" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":22, + "l":[ + "5_1e30956b0edd4dd8796a9c67ba4cb9627407c2fffdd5a2a395c6e38765a23fd7" + ] + } + } + ], + "s":{ + "v":{ + "s":"John" + }, + "i":"a069dc24" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":22, + "l":[ + "5_f325235338d2a8ecef054f2ecec90aaf0379de685445d59aac603253dad60093" + ] + } + } + ], + "s":{ + "v":{ + "s":"Mark" + }, + "i":"d7b02cc0" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"ab0b46ad" + }, + "intermediateFeature":{ + "t":0, + "r":[ + { + "c":[ + { + "p":{ + "f":"mainFeatureWithoutUserCondition", + "c":0, + "v":{ + "b":true + } + } + }, + { + "p":{ + "f":"mainFeatureWithoutUserCondition", + "c":0, + "v":{ + "b":true + } + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"570cf731" + } + } + ], + "v":{ + "b":false + }, + "i":"cdcafb72" + }, + "mainFeature":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "21_5067125a3533392865a118b2c79a4a52a00fee531910090a500832cfc6e8191f" + ] + } + }, + { + "u":{ + "a":"Country", + "c":16, + "l":[ + "27fc59bcf5b7bdfb1877ebc1e34b39ded9d557317ead60058e5a2be20949be56", + "613ba3556acefc0bdb54d92ec910c579234ae6b552fe215c17cd565e37f61735" + ] + } + } + ], + "s":{ + "v":{ + "s":"private" + }, + "i":"64f8e1a6" + } + }, + { + "c":[ + { + "u":{ + "a":"Country", + "c":16, + "l":[ + "daf7a57a85b7efa51d52f674d977987401ec6d9412d9e65586c2e2df9ea8b265" + ] + } + }, + { + "s":{ + "s":0, + "c":1 + } + }, + { + "s":{ + "s":1, + "c":1 + } + } + ], + "s":{ + "v":{ + "s":"target" + }, + "i":"f570ef26" + } + } + ], + "v":{ + "s":"public" + }, + "i":"f16ac582" + }, + "mainFeatureWithoutUserCondition":{ + "t":0, + "v":{ + "b":true + }, + "i":"1c6ca36e" + } + } + } + """.trimIndent() + override val tests: Array = arrayOf( + TestCase( + key = "dependentFeatureWithUserCondition", + defaultValue = "default", + returnValue = "Chicken", + user = null, + expectedLog = """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'.""" + ), + TestCase( + key = "dependentFeature", + defaultValue = "default", + returnValue = "Chicken", + user = null, + expectedLog = """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'.""" + ), + TestCase( + key = "dependentFeatureWithUserCondition2", + defaultValue = "default", + returnValue = "Frog", + user = null, + expectedLog = """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'.""" + ), + TestCase( + key = "dependentFeature", + defaultValue = "default", + returnValue = "Horse", + user = ConfigCatUser("12345", "kate@configcat.com", "USA"), + expectedLog = """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'.""" + ), + TestCase( + key = "dependentFeatureMultipleLevels", + defaultValue = "default", + returnValue = "Dog", + user = null, + expectedLog = """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/src/commonTest/kotlin/com/configcat/evaluation/data/SegmentTests.kt b/src/commonTest/kotlin/com/configcat/evaluation/data/SegmentTests.kt new file mode 100644 index 00000000..5578a3c4 --- /dev/null +++ b/src/commonTest/kotlin/com/configcat/evaluation/data/SegmentTests.kt @@ -0,0 +1,387 @@ +package com.configcat.evaluation.data + +import com.configcat.ConfigCatUser + +object SegmentTests : TestSet { + override val sdkKey = "configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/y_ZB7o-Xb0Swxth-ZlMSeA" + override val baseUrl = null + override val jsonOverride = """ +{ + "p":{ + "u":"https://cdn-global.configcat.com", + "r":0, + "s":"UZWYnRWPwF7hApMquVrUmyPRGziigICYz372JOYqXgw=" + }, + "s":[ + { + "n":"Beta users", + "r":[ + { + "a":"Email", + "c":16, + "l":[ + "89f6d080752f2969b6802c399e6141885c4ce40fb151f41b9ec955c1f4790490", + "2dde8bd2436cb07d45fb455847f8a09ea2427313c278b3352a39db31e6106c4c" + ] + } + ] + }, + { + "n":"Beta users (cleartext)", + "r":[ + { + "a":"Email", + "c":0, + "l":[ + "jane@example.com", + "john@example.com" + ] + } + ] + }, + { + "n":"Not Beta users", + "r":[ + { + "a":"Email", + "c":17, + "l":[ + "46e76bee50cb35e27095f4a624e8ba02a174f83cd062fb92975ea04fa0518a3f", + "274909972567e293a115dfdff5780c8aae7769a912ca596367e7d5523b8e8891" + ] + } + ] + }, + { + "n":"Not Beta users (cleartext)", + "r":[ + { + "a":"Email", + "c":1, + "l":[ + "jane@example.com", + "john@example.com" + ] + } + ] + } + ], + "f":{ + "featureWithNegatedSegmentTargeting":{ + "t":0, + "r":[ + { + "c":[ + { + "s":{ + "s":0, + "c":1 + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"772939a0" + } + } + ], + "v":{ + "b":false + }, + "i":"3c0be020" + }, + "featureWithNegatedSegmentTargetingCleartext":{ + "t":0, + "r":[ + { + "c":[ + { + "s":{ + "s":1, + "c":1 + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"0fc9b378" + } + } + ], + "v":{ + "b":false + }, + "i":"6c5f81e3" + }, + "featureWithNegatedSegmentTargetingInverse":{ + "t":0, + "r":[ + { + "c":[ + { + "s":{ + "s":2, + "c":1 + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"145a1eb0" + } + } + ], + "v":{ + "b":false + }, + "i":"e9c52981" + }, + "featureWithNegatedSegmentTargetingInverseCleartext":{ + "t":0, + "r":[ + { + "c":[ + { + "s":{ + "s":3, + "c":1 + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"4898b966" + } + } + ], + "v":{ + "b":false + }, + "i":"fa8f80d5" + }, + "featureWithSegmentTargeting":{ + "t":0, + "r":[ + { + "c":[ + { + "s":{ + "s":0, + "c":0 + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"a49f6150" + } + } + ], + "v":{ + "b":false + }, + "i":"cfe41874" + }, + "featureWithSegmentTargetingCleartext":{ + "t":0, + "r":[ + { + "c":[ + { + "s":{ + "s":1, + "c":0 + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"d03ed88c" + } + } + ], + "v":{ + "b":false + }, + "i":"89fac05a" + }, + "featureWithSegmentTargetingInverse":{ + "t":0, + "r":[ + { + "c":[ + { + "s":{ + "s":2, + "c":0 + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"cf444ba3" + } + } + ], + "v":{ + "b":false + }, + "i":"2ddaee84" + }, + "featureWithSegmentTargetingInverseCleartext":{ + "t":0, + "r":[ + { + "c":[ + { + "s":{ + "s":3, + "c":0 + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"a78fc410" + } + } + ], + "v":{ + "b":false + }, + "i":"6a3224de" + }, + "featureWithSegmentTargetingMultipleConditions":{ + "t":0, + "r":[ + { + "c":[ + { + "s":{ + "s":1, + "c":0 + } + }, + { + "u":{ + "a":"Email", + "c":32, + "l":[ + "@example.com" + ] + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"dffdf084" + } + } + ], + "v":{ + "b":false + }, + "i":"3f2ec515" + } + } +} + """.trimIndent() + override val tests: Array = arrayOf( + TestCase( + key = "featureWithSegmentTargeting", + defaultValue = false, + returnValue = false, + user = null, + expectedLog = """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'.""" + ), + TestCase( + key = "featureWithSegmentTargeting", + defaultValue = false, + returnValue = true, + user = ConfigCatUser("12345", "jane@example.com"), + expectedLog = """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'.""" + ), + TestCase( + key = "featureWithNegatedSegmentTargeting", + defaultValue = false, + returnValue = false, + user = ConfigCatUser("12345", "jane@example.com"), + expectedLog = """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'.""" + ), + TestCase( + key = "featureWithNegatedSegmentTargetingCleartext", + defaultValue = false, + returnValue = false, + user = ConfigCatUser("12345"), + expectedLog = """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'.""" + ), + TestCase( + key = "featureWithSegmentTargetingMultipleConditions", + defaultValue = false, + returnValue = false, + user = null, + expectedLog = """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/src/commonTest/kotlin/com/configcat/evaluation/data/SemverValidationTests.kt b/src/commonTest/kotlin/com/configcat/evaluation/data/SemverValidationTests.kt new file mode 100644 index 00000000..e825d8d6 --- /dev/null +++ b/src/commonTest/kotlin/com/configcat/evaluation/data/SemverValidationTests.kt @@ -0,0 +1,545 @@ +package com.configcat.evaluation.data + +import com.configcat.ConfigCatUser + +object SemverValidationTests : TestSet { + override val sdkKey = "PKDVCLf-Hq-h-kCzMp-L7Q/BAr3KgLTP0ObzKnBTo5nhA" + override val baseUrl = null + override val jsonOverride = """ + { + "p":{ + "u":"https://cdn-global.configcat.com", + "r":0, + "s":"qDuANLZ34LaUsBhUchzN4YbCzo4Jx9HhwUYnsCujKZU=" + }, + "f":{ + "isNotOneOf":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":5, + "l":[ + "1.0.0", + "1.0.1", + "2.0.0", + "2.0.1", + "2.0.2", + "" + ] + } + } + ], + "s":{ + "v":{ + "s":"Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, )" + }, + "i":"a8d5f278" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":5, + "l":[ + "1.0.0", + "3.0.1" + ] + } + } + ], + "s":{ + "v":{ + "s":"Is not one of (1.0.0, 3.0.1)" + }, + "i":"54ac757f" + } + } + ], + "v":{ + "s":"Default" + }, + "i":"f79b763d" + }, + "isNotOneOfWithPercentage":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":5, + "l":[ + "1.0.0", + "1.0.1", + "2.0.0", + "2.0.1", + "2.0.2", + "" + ] + } + } + ], + "s":{ + "v":{ + "s":"Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, )" + }, + "i":"9bf9e66f" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":5, + "l":[ + "1.0.0", + "3.0.1" + ] + } + } + ], + "s":{ + "v":{ + "s":"Is not one of (1.0.0, 3.0.1)" + }, + "i":"bfc1a544" + } + } + ], + "p":[ + { + "p":20, + "v":{ + "s":"20%" + }, + "i":"68f652f0" + }, + { + "p":80, + "v":{ + "s":"80%" + }, + "i":"b8d926e0" + } + ], + "v":{ + "s":"Default" + }, + "i":"b9614bad" + }, + "isOneOf":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":4, + "l":[ + "1.0.0", + "2" + ] + } + } + ], + "s":{ + "v":{ + "s":"Is one of (1.0.0, 2)" + }, + "i":"1e934047" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":4, + "l":[ + "1.0.0" + ] + } + } + ], + "s":{ + "v":{ + "s":"Is one of (1.0.0)" + }, + "i":"44342254" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":4, + "l":[ + "", + "2.0.1", + "2.0.2" + ] + } + } + ], + "s":{ + "v":{ + "s":"Is one of ( , 2.0.1, 2.0.2, )" + }, + "i":"90e3ef46" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":4, + "l":[ + "3......" + ] + } + } + ], + "s":{ + "v":{ + "s":"Is one of (3......)" + }, + "i":"59523971" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":4, + "l":[ + "3...." + ] + } + } + ], + "s":{ + "v":{ + "s":"Is one of (3...)" + }, + "i":"2de217a1" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":4, + "l":[ + "3..0" + ] + } + } + ], + "s":{ + "v":{ + "s":"Is one of (3..0)" + }, + "i":"bf943c79" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":4, + "l":[ + "3.0" + ] + } + } + ], + "s":{ + "v":{ + "s":"Is one of (3.0)" + }, + "i":"3a6a8077" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":4, + "l":[ + "3.0." + ] + } + } + ], + "s":{ + "v":{ + "s":"Is one of (3.0.)" + }, + "i":"44f25fed" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":4, + "l":[ + "3.0.0" + ] + } + } + ], + "s":{ + "v":{ + "s":"Is one of (3.0.0)" + }, + "i":"e77f5306" + } + } + ], + "v":{ + "s":"Default" + }, + "i":"c4ec4d53" + }, + "isOneOfWithPercentage":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":4, + "l":[ + "1.0.0" + ] + } + } + ], + "s":{ + "v":{ + "s":"is one of (1.0.0)" + }, + "i":"0ac4afc1" + } + } + ], + "p":[ + { + "p":20, + "v":{ + "s":"20%" + }, + "i":"e25dba31" + }, + { + "p":80, + "v":{ + "s":"80%" + }, + "i":"8c70c181" + } + ], + "v":{ + "s":"Default" + }, + "i":"a94ff896" + }, + "lessThanWithPercentage":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":6, + "s":"1.0.0" + } + } + ], + "s":{ + "v":{ + "s":"\u003C 1.0.0" + }, + "i":"0c27d053" + } + } + ], + "p":[ + { + "p":20, + "v":{ + "s":"20%" + }, + "i":"3b1fde2a" + }, + { + "p":80, + "v":{ + "s":"80%" + }, + "i":"42e92759" + } + ], + "v":{ + "s":"Default" + }, + "i":"0081c525" + }, + "relations":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":6, + "s":"1.0.0," + } + } + ], + "s":{ + "v":{ + "s":"\u003C1.0.0," + }, + "i":"21b31b61" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":6, + "s":"1.0.0" + } + } + ], + "s":{ + "v":{ + "s":"\u003C 1.0.0" + }, + "i":"db3ddb7d" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":7, + "s":"1.0.0" + } + } + ], + "s":{ + "v":{ + "s":"\u003C=1.0.0" + }, + "i":"aa2c7493" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":8, + "s":"2.0.0" + } + } + ], + "s":{ + "v":{ + "s":"\u003E2.0.0" + }, + "i":"5e47a1ea" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":9, + "s":"2.0.0" + } + } + ], + "s":{ + "v":{ + "s":"\u003E=2.0.0" + }, + "i":"99482756" + } + } + ], + "v":{ + "s":"Default" + }, + "i":"c6155773" + } + } +} + """.trimIndent() + override val tests: Array = arrayOf( + TestCase( + key = "isNotOneOf", + defaultValue = "default", + returnValue = "Default", + user = ConfigCatUser("12345", custom = mapOf("Custom1" to "wrong_semver")), + expectedLog = """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'.""" + ), + TestCase( + key = "relations", + defaultValue = "default", + returnValue = "Default", + user = ConfigCatUser("12345", custom = mapOf("Custom1" to "wrong_semver")), + expectedLog = """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/src/commonTest/kotlin/com/configcat/evaluation/data/SimpleValueTests.kt b/src/commonTest/kotlin/com/configcat/evaluation/data/SimpleValueTests.kt new file mode 100644 index 00000000..e355e0ec --- /dev/null +++ b/src/commonTest/kotlin/com/configcat/evaluation/data/SimpleValueTests.kt @@ -0,0 +1,642 @@ +package com.configcat.evaluation.data + +object SimpleValueTests : TestSet { + override val sdkKey = "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A" + override val baseUrl = null + override val jsonOverride = """ + { + "p":{ + "u":"https://cdn-global.configcat.com", + "r":0, + "s":"pkw2BWOIXiTrXO53/OPECHP9OeJzmW8y/yV47\u002BQ8HLM=" + }, + "f":{ + "bool30TrueAdvancedRules":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":0, + "l":[ + "a@configcat.com", + "b@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "b":false + }, + "i":"385d9803" + } + }, + { + "c":[ + { + "u":{ + "a":"Country", + "c":2, + "l":[ + "United" + ] + } + } + ], + "s":{ + "v":{ + "b":false + }, + "i":"385d9803" + } + } + ], + "p":[ + { + "p":30, + "v":{ + "b":true + }, + "i":"607147d5" + }, + { + "p":70, + "v":{ + "b":false + }, + "i":"385d9803" + } + ], + "v":{ + "b":true + }, + "i":"607147d5" + }, + "boolDefaultFalse":{ + "t":0, + "v":{ + "b":false + }, + "i":"489a16d2" + }, + "boolDefaultTrue":{ + "t":0, + "v":{ + "b":true + }, + "i":"09513143" + }, + "double25Pi25E25Gr25Zero":{ + "t":3, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "d":5.561 + }, + "i":"3f7826de" + } + } + ], + "p":[ + { + "p":25, + "v":{ + "d":3.1415 + }, + "i":"6d75b4d3" + }, + { + "p":25, + "v":{ + "d":2.7182 + }, + "i":"183ee713" + }, + { + "p":25, + "v":{ + "d":1.61803 + }, + "i":"01eb6326" + }, + { + "p":25, + "v":{ + "d":0 + }, + "i":"64c434ff" + } + ], + "v":{ + "d":-1 + }, + "i":"9503a1de" + }, + "doubleDefaultPi":{ + "t":3, + "v":{ + "d":3.1415 + }, + "i":"5af8acc7" + }, + "integer25One25Two25Three25FourAdvancedRules":{ + "t":2, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "i":5 + }, + "i":"58136ba2" + } + } + ], + "p":[ + { + "p":25, + "v":{ + "i":1 + }, + "i":"11634414" + }, + { + "p":25, + "v":{ + "i":2 + }, + "i":"5530655d" + }, + { + "p":25, + "v":{ + "i":3 + }, + "i":"2ad19a52" + }, + { + "p":25, + "v":{ + "i":4 + }, + "i":"41b30851" + } + ], + "v":{ + "i":-1 + }, + "i":"ce3c4f5a" + }, + "integerDefaultOne":{ + "t":2, + "v":{ + "i":1 + }, + "i":"faadbf54" + }, + "keySampleText":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Country", + "c":0, + "l":[ + "Hungary", + "Bahamas" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"9fa0e57e" + } + }, + { + "c":[ + { + "u":{ + "a":"SubscriptionType", + "c":0, + "l":[ + "unlimited" + ] + } + } + ], + "s":{ + "v":{ + "s":"Lion" + }, + "i":"2be6b03f" + } + } + ], + "p":[ + { + "p":50, + "v":{ + "s":"Falcon" + }, + "i":"baff2362" + }, + { + "p":50, + "v":{ + "s":"Horse" + }, + "i":"dab78ba5" + } + ], + "v":{ + "s":"Cat" + }, + "i":"69ef126c" + }, + "string25Cat25Dog25Falcon25Horse":{ + "t":1, + "p":[ + { + "p":25, + "v":{ + "s":"Cat" + }, + "i":"d227b334" + }, + { + "p":25, + "v":{ + "s":"Dog" + }, + "i":"622f5d07" + }, + { + "p":25, + "v":{ + "s":"Falcon" + }, + "i":"0ff32bab" + }, + { + "p":25, + "v":{ + "s":"Horse" + }, + "i":"6c597441" + } + ], + "v":{ + "s":"Chicken" + }, + "i":"2588a3e6" + }, + "string25Cat25Dog25Falcon25HorseAdvancedRules":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Country", + "c":0, + "l":[ + "Hungary", + "United Kingdom" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dolphin" + }, + "i":"3accb1d0" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":2, + "l":[ + "admi" + ] + } + } + ], + "s":{ + "v":{ + "s":"Lion" + }, + "i":"e95ebf10" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Kitten" + }, + "i":"88243650" + } + } + ], + "p":[ + { + "p":25, + "v":{ + "s":"Cat" + }, + "i":"83461b47" + }, + { + "p":25, + "v":{ + "s":"Dog" + }, + "i":"4f026fbc" + }, + { + "p":25, + "v":{ + "s":"Falcon" + }, + "i":"392a4d59" + }, + { + "p":25, + "v":{ + "s":"Horse" + }, + "i":"bb66b1f3" + } + ], + "v":{ + "s":"Chicken" + }, + "i":"8250ef5a" + }, + "string75Cat0Dog25Falcon0Horse":{ + "t":1, + "p":[ + { + "p":75, + "v":{ + "s":"Cat" + }, + "i":"93f5a1c0" + }, + { + "p":0, + "v":{ + "s":"Dog" + }, + "i":"b8f49554" + }, + { + "p":25, + "v":{ + "s":"Falcon" + }, + "i":"7beaf504" + }, + { + "p":0, + "v":{ + "s":"Horse" + }, + "i":"30ee31af" + } + ], + "v":{ + "s":"Chicken" + }, + "i":"aa65b5ce" + }, + "stringContainsDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"d0cd8f06" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"ce564c3a" + }, + "stringDefaultCat":{ + "t":1, + "v":{ + "s":"Cat" + }, + "i":"7a0be518" + }, + "stringIsInDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":0, + "l":[ + "a@configcat.com", + "b@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"5b64d9b4" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":0, + "l":[ + "admin" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"5b64d9b4" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"83372510" + }, + "stringIsNotInDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":1, + "l":[ + "a@configcat.com", + "b@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"6ada5ff2" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"2459598d" + }, + "stringNotContainsDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":3, + "l":[ + "@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"f7f8f43d" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"44ab483a" + } + } +} + """.trimIndent() + override val tests: Array = arrayOf( + TestCase( + key = "boolDefaultFalse", + defaultValue = true, + returnValue = false, + expectedLog = """INFO [5000] Evaluating 'boolDefaultFalse' + Returning 'false'.""", + user = null + ), + TestCase( + key = "boolDefaultTrue", + defaultValue = false, + returnValue = true, + expectedLog = """INFO [5000] Evaluating 'boolDefaultTrue' + Returning 'true'.""", + user = null + ), + TestCase( + key = "stringDefaultCat", + defaultValue = "stringDefaultCat", + returnValue = "Cat", + expectedLog = """INFO [5000] Evaluating 'stringDefaultCat' + Returning 'Cat'.""", + user = null + ), + TestCase( + key = "integerDefaultOne", + defaultValue = 0, + returnValue = 1, + expectedLog = """INFO [5000] Evaluating 'integerDefaultOne' + Returning '1'.""", + user = null + ), + TestCase( + key = "doubleDefaultPi", + defaultValue = 0.0, + returnValue = 3.1415, + expectedLog = """INFO [5000] Evaluating 'doubleDefaultPi' + Returning '3.1415'.""", + user = null + ) + ) +} diff --git a/src/commonTest/kotlin/com/configcat/evaluation/data/TestCase.kt b/src/commonTest/kotlin/com/configcat/evaluation/data/TestCase.kt new file mode 100644 index 00000000..554350f4 --- /dev/null +++ b/src/commonTest/kotlin/com/configcat/evaluation/data/TestCase.kt @@ -0,0 +1,11 @@ +package com.configcat.evaluation.data + +import com.configcat.ConfigCatUser + +data class TestCase( + val key: String, + val defaultValue: Any, + val returnValue: Any, + val expectedLog: String?, + val user: ConfigCatUser? +) diff --git a/src/commonTest/kotlin/com/configcat/evaluation/data/TestSet.kt b/src/commonTest/kotlin/com/configcat/evaluation/data/TestSet.kt new file mode 100644 index 00000000..efd646cd --- /dev/null +++ b/src/commonTest/kotlin/com/configcat/evaluation/data/TestSet.kt @@ -0,0 +1,8 @@ +package com.configcat.evaluation.data + +interface TestSet { + val sdkKey: String? + val baseUrl: String? + val jsonOverride: String + val tests: Array? +} diff --git a/src/commonTest/kotlin/com/configcat/evaluation/data/TwoTargetingRulesTests.kt b/src/commonTest/kotlin/com/configcat/evaluation/data/TwoTargetingRulesTests.kt new file mode 100644 index 00000000..8de27964 --- /dev/null +++ b/src/commonTest/kotlin/com/configcat/evaluation/data/TwoTargetingRulesTests.kt @@ -0,0 +1,659 @@ +package com.configcat.evaluation.data + +import com.configcat.ConfigCatUser + +object TwoTargetingRulesTests : TestSet { + override val sdkKey = "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A" + override val baseUrl = null + override val jsonOverride = """ + { + "p":{ + "u":"https://cdn-global.configcat.com", + "r":0, + "s":"pkw2BWOIXiTrXO53/OPECHP9OeJzmW8y/yV47\u002BQ8HLM=" + }, + "f":{ + "bool30TrueAdvancedRules":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":0, + "l":[ + "a@configcat.com", + "b@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "b":false + }, + "i":"385d9803" + } + }, + { + "c":[ + { + "u":{ + "a":"Country", + "c":2, + "l":[ + "United" + ] + } + } + ], + "s":{ + "v":{ + "b":false + }, + "i":"385d9803" + } + } + ], + "p":[ + { + "p":30, + "v":{ + "b":true + }, + "i":"607147d5" + }, + { + "p":70, + "v":{ + "b":false + }, + "i":"385d9803" + } + ], + "v":{ + "b":true + }, + "i":"607147d5" + }, + "boolDefaultFalse":{ + "t":0, + "v":{ + "b":false + }, + "i":"489a16d2" + }, + "boolDefaultTrue":{ + "t":0, + "v":{ + "b":true + }, + "i":"09513143" + }, + "double25Pi25E25Gr25Zero":{ + "t":3, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "d":5.561 + }, + "i":"3f7826de" + } + } + ], + "p":[ + { + "p":25, + "v":{ + "d":3.1415 + }, + "i":"6d75b4d3" + }, + { + "p":25, + "v":{ + "d":2.7182 + }, + "i":"183ee713" + }, + { + "p":25, + "v":{ + "d":1.61803 + }, + "i":"01eb6326" + }, + { + "p":25, + "v":{ + "d":0 + }, + "i":"64c434ff" + } + ], + "v":{ + "d":-1 + }, + "i":"9503a1de" + }, + "doubleDefaultPi":{ + "t":3, + "v":{ + "d":3.1415 + }, + "i":"5af8acc7" + }, + "integer25One25Two25Three25FourAdvancedRules":{ + "t":2, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "i":5 + }, + "i":"58136ba2" + } + } + ], + "p":[ + { + "p":25, + "v":{ + "i":1 + }, + "i":"11634414" + }, + { + "p":25, + "v":{ + "i":2 + }, + "i":"5530655d" + }, + { + "p":25, + "v":{ + "i":3 + }, + "i":"2ad19a52" + }, + { + "p":25, + "v":{ + "i":4 + }, + "i":"41b30851" + } + ], + "v":{ + "i":-1 + }, + "i":"ce3c4f5a" + }, + "integerDefaultOne":{ + "t":2, + "v":{ + "i":1 + }, + "i":"faadbf54" + }, + "keySampleText":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Country", + "c":0, + "l":[ + "Hungary", + "Bahamas" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"9fa0e57e" + } + }, + { + "c":[ + { + "u":{ + "a":"SubscriptionType", + "c":0, + "l":[ + "unlimited" + ] + } + } + ], + "s":{ + "v":{ + "s":"Lion" + }, + "i":"2be6b03f" + } + } + ], + "p":[ + { + "p":50, + "v":{ + "s":"Falcon" + }, + "i":"baff2362" + }, + { + "p":50, + "v":{ + "s":"Horse" + }, + "i":"dab78ba5" + } + ], + "v":{ + "s":"Cat" + }, + "i":"69ef126c" + }, + "string25Cat25Dog25Falcon25Horse":{ + "t":1, + "p":[ + { + "p":25, + "v":{ + "s":"Cat" + }, + "i":"d227b334" + }, + { + "p":25, + "v":{ + "s":"Dog" + }, + "i":"622f5d07" + }, + { + "p":25, + "v":{ + "s":"Falcon" + }, + "i":"0ff32bab" + }, + { + "p":25, + "v":{ + "s":"Horse" + }, + "i":"6c597441" + } + ], + "v":{ + "s":"Chicken" + }, + "i":"2588a3e6" + }, + "string25Cat25Dog25Falcon25HorseAdvancedRules":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Country", + "c":0, + "l":[ + "Hungary", + "United Kingdom" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dolphin" + }, + "i":"3accb1d0" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":2, + "l":[ + "admi" + ] + } + } + ], + "s":{ + "v":{ + "s":"Lion" + }, + "i":"e95ebf10" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Kitten" + }, + "i":"88243650" + } + } + ], + "p":[ + { + "p":25, + "v":{ + "s":"Cat" + }, + "i":"83461b47" + }, + { + "p":25, + "v":{ + "s":"Dog" + }, + "i":"4f026fbc" + }, + { + "p":25, + "v":{ + "s":"Falcon" + }, + "i":"392a4d59" + }, + { + "p":25, + "v":{ + "s":"Horse" + }, + "i":"bb66b1f3" + } + ], + "v":{ + "s":"Chicken" + }, + "i":"8250ef5a" + }, + "string75Cat0Dog25Falcon0Horse":{ + "t":1, + "p":[ + { + "p":75, + "v":{ + "s":"Cat" + }, + "i":"93f5a1c0" + }, + { + "p":0, + "v":{ + "s":"Dog" + }, + "i":"b8f49554" + }, + { + "p":25, + "v":{ + "s":"Falcon" + }, + "i":"7beaf504" + }, + { + "p":0, + "v":{ + "s":"Horse" + }, + "i":"30ee31af" + } + ], + "v":{ + "s":"Chicken" + }, + "i":"aa65b5ce" + }, + "stringContainsDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"d0cd8f06" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"ce564c3a" + }, + "stringDefaultCat":{ + "t":1, + "v":{ + "s":"Cat" + }, + "i":"7a0be518" + }, + "stringIsInDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":0, + "l":[ + "a@configcat.com", + "b@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"5b64d9b4" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":0, + "l":[ + "admin" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"5b64d9b4" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"83372510" + }, + "stringIsNotInDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":1, + "l":[ + "a@configcat.com", + "b@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"6ada5ff2" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"2459598d" + }, + "stringNotContainsDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":3, + "l":[ + "@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"f7f8f43d" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"44ab483a" + } + } +} + """.trimIndent() + override val tests: Array = arrayOf( + TestCase( + key = "stringIsInDogDefaultCat", + defaultValue = "default", + returnValue = "Cat", + user = null, + expectedLog = """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'.""" + ), + TestCase( + key = "stringIsInDogDefaultCat", + defaultValue = "default", + returnValue = "Cat", + user = ConfigCatUser("12345"), + expectedLog = """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'.""" + ), + TestCase( + key = "stringIsInDogDefaultCat", + defaultValue = "default", + returnValue = "Cat", + user = ConfigCatUser("12345", custom = mapOf("Custom1" to "user")), + expectedLog = """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'.""" + ), + TestCase( + key = "stringIsInDogDefaultCat", + defaultValue = "default", + returnValue = "Dog", + user = ConfigCatUser("12345", custom = mapOf("Custom1" to "admin")), + expectedLog = """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/src/commonTest/kotlin/com/configcat/integration/RolloutMatrixTests.kt b/src/commonTest/kotlin/com/configcat/integration/RolloutMatrixTests.kt index 04b796f7..57081e24 100644 --- a/src/commonTest/kotlin/com/configcat/integration/RolloutMatrixTests.kt +++ b/src/commonTest/kotlin/com/configcat/integration/RolloutMatrixTests.kt @@ -2,8 +2,8 @@ package com.configcat.integration import com.configcat.ConfigCatClient import com.configcat.ConfigCatUser -import com.configcat.getValueDetails import com.configcat.integration.matrix.* +import com.configcat.log.LogLevel import com.configcat.manualPoll import io.ktor.client.engine.mock.* import io.ktor.http.* @@ -50,6 +50,36 @@ class RolloutMatrixTests { runMatrixTest(VariationIdMatrix, false) } + @Test + fun testAndOrMatrix() = runTest { + runMatrixTest(AndOrMatrix, true) + } + + @Test + fun testComparatorsV6Matrix() = runTest { + runMatrixTest(ComparatorsV6Matrix, true) + } + + @Test + fun testPrerequisiteFlagMatrix() = runTest { + runMatrixTest(PrerequisiteFlagMatrix, true) + } + + @Test + fun testSegmentMatrix() = runTest { + runMatrixTest(SegmentMatrix, true) + } + + @Test + fun testSegmentsOldMatrix() = runTest { + runMatrixTest(SegmentsOldMatrix, true) + } + + @Test + fun testUnicodeMatrix() = runTest { + runMatrixTest(UnicodeMatrix, true) + } + private suspend fun runMatrixTest(matrix: DataMatrix, isValueKind: Boolean) { val mockEngine = MockEngine { respond(content = matrix.remoteJson, status = HttpStatusCode.OK) @@ -57,6 +87,7 @@ class RolloutMatrixTests { val client = ConfigCatClient(matrix.sdkKey) { pollingMode = manualPoll() httpEngine = mockEngine + logLevel = LogLevel.ERROR } client.forceRefresh() @@ -91,7 +122,7 @@ class RolloutMatrixTests { for ((j, settingKey) in settingKeys.withIndex()) { if (isValueKind) { - val value = client.getAnyValue(settingKey, "", user) + val value = client.getAnyValue(settingKey, null, user) val boolVal = value as? Boolean if (boolVal != null) { val expected = testObjects[j + 4].lowercase().toBooleanStrictOrNull() @@ -122,7 +153,7 @@ class RolloutMatrixTests { errors.add("Identifier: ${testObjects[0]}, Key: $settingKey. UV: ${testObjects[3]} Expected: $expected, Result: $value") } } else { - val variationId = client.getValueDetails(settingKey, "", user).variationId + val variationId = client.getAnyValueDetails(settingKey, null, user).variationId if (variationId != testObjects[j + 4]) { errors.add("Identifier: ${testObjects[0]}, Key: $settingKey. UV: ${testObjects[3]} Expected: ${testObjects[j + 4]}, Result: $variationId") } diff --git a/src/commonTest/kotlin/com/configcat/integration/matrix/AndOrMatrix.kt b/src/commonTest/kotlin/com/configcat/integration/matrix/AndOrMatrix.kt new file mode 100644 index 00000000..55197b9e --- /dev/null +++ b/src/commonTest/kotlin/com/configcat/integration/matrix/AndOrMatrix.kt @@ -0,0 +1,449 @@ +package com.configcat.integration.matrix + +object AndOrMatrix : DataMatrix { + override val sdkKey: String = "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/ByMO9yZNn02kXcm72lnY1A" + override val data = + """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""" + override val remoteJson = + """{ + "p":{ + "u":"https://cdn-global.configcat.com", + "r":0, + "s":"wp/t/G2kXSBXcxpSHiS0MV9ua/jmPiNQciQl2KiofTE=" + }, + "s":[ + { + "n":"Beta Users", + "r":[ + { + "a":"Email", + "c":16, + "l":[ + "42419377a50e100e5043bb0cf2f51edf40100fc950812e2ef621ff4230cc7440", + "5eb6b039d5eb5e65c14f9f77ae02cc2f569e3a10770c5275b6efc43c42436721" + ] + } + ] + }, + { + "n":"Developers", + "r":[ + { + "a":"Email", + "c":16, + "l":[ + "e7eee78965dcd30dceef765b03794fbe13192044c7dd241140ab067042dbb917", + "793c3a66237dc3ace5d58ff077368833545951ebc4c29259309bb0c42582e170" + ] + } + ] + } + ], + "f":{ + "dependentFeature":{ + "t":1, + "r":[ + { + "c":[ + { + "p":{ + "f":"mainFeature", + "c":0, + "v":{ + "s":"target" + } + } + } + ], + "p":[ + { + "p":25, + "v":{ + "s":"Cat" + }, + "i":"993d7ee0" + }, + { + "p":25, + "v":{ + "s":"Dog" + }, + "i":"08b8348e" + }, + { + "p":25, + "v":{ + "s":"Falcon" + }, + "i":"a6fb7a01" + }, + { + "p":25, + "v":{ + "s":"Horse" + }, + "i":"699fb4bf" + } + ] + } + ], + "v":{ + "s":"Chicken" + }, + "i":"e6198f92" + }, + "dependentFeatureWithUserCondition":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":16, + "l":[ + "947316bbbba2167afbd3814d3f6b587646d7bc5aca7f3aa02432bebdbf17bf67", + "70e863f78cf09a78ee4f5eb18f29d4a9ebee1016a4973625b1ead19ad1386ff9" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"ef802e43" + } + }, + { + "c":[ + { + "p":{ + "f":"mainFeatureWithoutUserCondition", + "c":0, + "v":{ + "b":true + } + } + } + ], + "p":[ + { + "p":34, + "v":{ + "s":"Cat" + }, + "i":"4a65d6ef" + }, + { + "p":33, + "v":{ + "s":"Horse" + }, + "i":"fc3bb22b" + }, + { + "p":33, + "v":{ + "s":"Falcon" + }, + "i":"32e0e525" + } + ] + } + ], + "v":{ + "s":"Chicken" + }, + "i":"472e10f4" + }, + "dependentFeatureWithUserCondition2":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":16, + "l":[ + "4f9261e69d815dcb4f80597f39955de15f417ba90679fc9b247f03b18283946d", + "cb9f65999c1d16661793e270887bc3bcda99d2053737faf11a9ffc03b3963f2e" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"78eceed0" + } + }, + { + "c":[ + { + "p":{ + "f":"mainFeature", + "c":0, + "v":{ + "s":"public" + } + } + } + ], + "p":[ + { + "p":34, + "v":{ + "s":"Cat" + }, + "i":"72b97d0e" + }, + { + "p":33, + "v":{ + "s":"Horse" + }, + "i":"81846c69" + }, + { + "p":33, + "v":{ + "s":"Falcon" + }, + "i":"e2f3b509" + } + ] + }, + { + "c":[ + { + "p":{ + "f":"mainFeature", + "c":0, + "v":{ + "s":"public" + } + } + } + ], + "s":{ + "v":{ + "s":"Frog" + }, + "i":"cfd56c79" + } + } + ], + "v":{ + "s":"Chicken" + }, + "i":"9e8d62c6" + }, + "emailAnd":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":22, + "l":[ + "4_127aa63815a00ec48882875324035a226eedfec3a6785d5fa25a7924a85c3ad4" + ] + } + }, + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@" + ] + } + }, + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "20_a215f31a501e2ea8cf2b0bd04b09ddbc3a8e95b18a33df4e7b73e3f0f9859329" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"a1393561" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"bdabd589" + }, + "emailOr":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":22, + "l":[ + "5_6305b8a9a93692f85bca7e25d24d5d55f7e54c5d5e3518281ac72391b3c076fa" + ] + } + } + ], + "s":{ + "v":{ + "s":"Jane" + }, + "i":"01383bbf" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":22, + "l":[ + "5_9892bda500d2af98d78302a8b21c66558d7af5e60b813ac7d86bc0a24a6a7890" + ] + } + } + ], + "s":{ + "v":{ + "s":"John" + }, + "i":"a069dc24" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":22, + "l":[ + "5_d5492107764a64aa36ab7ef0897ad90d58e42bc40a9d349116eed29376bc9283" + ] + } + } + ], + "s":{ + "v":{ + "s":"Mark" + }, + "i":"d7b02cc0" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"ab0b46ad" + }, + "mainFeature":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "21_fc5f667e8d3ca3d70d9d08e0122158bb0ac4ff309796f74e67ffb8875ff56535" + ] + } + }, + { + "u":{ + "a":"Country", + "c":16, + "l":[ + "ca8b73be854eba07330afe5f23d88321d3e244e079f72a8bcffbd621e6efcc97", + "7af1edb337f921cc1b10922987a17b1d816be9e0bbb9c20ea1cdd913e7eb5866" + ] + } + } + ], + "s":{ + "v":{ + "s":"private" + }, + "i":"64f8e1a6" + } + }, + { + "c":[ + { + "u":{ + "a":"Country", + "c":16, + "l":[ + "34682b8157c70f7d64240b7dfdeccbc0b991ca18d68cb5c62e9c88f242dba8ed" + ] + } + }, + { + "s":{ + "s":0, + "c":1 + } + }, + { + "s":{ + "s":1, + "c":1 + } + } + ], + "s":{ + "v":{ + "s":"target" + }, + "i":"f570ef26" + } + } + ], + "v":{ + "s":"public" + }, + "i":"f16ac582" + }, + "mainFeatureWithoutUserCondition":{ + "t":0, + "v":{ + "b":true + }, + "i":"1c6ca36e" + } + } +} +""" +} diff --git a/src/commonTest/kotlin/com/configcat/integration/matrix/ComparatorsV6Matrix.kt b/src/commonTest/kotlin/com/configcat/integration/matrix/ComparatorsV6Matrix.kt new file mode 100644 index 00000000..2b2f2623 --- /dev/null +++ b/src/commonTest/kotlin/com/configcat/integration/matrix/ComparatorsV6Matrix.kt @@ -0,0 +1,1409 @@ +package com.configcat.integration.matrix + +object ComparatorsV6Matrix : DataMatrix { + override val sdkKey: String = "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ" + override val data = + """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""" + override val remoteJson = + """{ + "p":{ + "u":"https://cdn-global.configcat.com", + "r":0, + "s":"JEl\u002BhoGfr/01JCnpxr7kOCIoB2bYAM3uTMShm6HiAc4=" + }, + "f":{ + "allinone":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":20, + "s":"0740e9103e26acb85b086dd9a15eaa84246902f096655371a5f001b9e13754e8" + } + }, + { + "u":{ + "a":"Email", + "c":21, + "s":"0740e9103e26acb85b086dd9a15eaa84246902f096655371a5f001b9e13754e8" + } + } + ], + "s":{ + "v":{ + "s":"1h" + }, + "i":"e3a79156" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":28, + "s":"joe@example.com" + } + }, + { + "u":{ + "a":"Email", + "c":29, + "s":"joe@example.com" + } + } + ], + "s":{ + "v":{ + "s":"1c" + }, + "i":"ed60451a" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":16, + "l":[ + "0740e9103e26acb85b086dd9a15eaa84246902f096655371a5f001b9e13754e8" + ] + } + }, + { + "u":{ + "a":"Email", + "c":17, + "l":[ + "0740e9103e26acb85b086dd9a15eaa84246902f096655371a5f001b9e13754e8" + ] + } + } + ], + "s":{ + "v":{ + "s":"2h" + }, + "i":"aa24b7a3" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":0, + "l":[ + "joe@example.com" + ] + } + }, + { + "u":{ + "a":"Email", + "c":1, + "l":[ + "joe@example.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"2c" + }, + "i":"d37425a1" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":22, + "l":[ + "4_e99c716658ca0b1035394161a3ca54f8dc688930ad90bed26aeff075cb947397" + ] + } + }, + { + "u":{ + "a":"Email", + "c":23, + "l":[ + "4_e99c716658ca0b1035394161a3ca54f8dc688930ad90bed26aeff075cb947397" + ] + } + } + ], + "s":{ + "v":{ + "s":"3h" + }, + "i":"5e6e0c6c" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":30, + "l":[ + "joe@" + ] + } + }, + { + "u":{ + "a":"Email", + "c":31, + "l":[ + "joe@" + ] + } + } + ], + "s":{ + "v":{ + "s":"3c" + }, + "i":"5f562a70" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "12_29030906a5c2729247ccad10154b56b84d61ee4d732361e0ba7c3817da4f91b3" + ] + } + }, + { + "u":{ + "a":"Email", + "c":25, + "l":[ + "12_29030906a5c2729247ccad10154b56b84d61ee4d732361e0ba7c3817da4f91b3" + ] + } + } + ], + "s":{ + "v":{ + "s":"4h" + }, + "i":"91b91d69" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":32, + "l":[ + "@example.com" + ] + } + }, + { + "u":{ + "a":"Email", + "c":33, + "l":[ + "@example.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"4c" + }, + "i":"4c80a977" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "e@e" + ] + } + }, + { + "u":{ + "a":"Email", + "c":3, + "l":[ + "e@e" + ] + } + } + ], + "s":{ + "v":{ + "s":"5" + }, + "i":"dd12c429" + } + }, + { + "c":[ + { + "u":{ + "a":"Version", + "c":4, + "l":[ + "1.0.0" + ] + } + }, + { + "u":{ + "a":"Version", + "c":5, + "l":[ + "1.0.0" + ] + } + } + ], + "s":{ + "v":{ + "s":"6" + }, + "i":"dba5d266" + } + }, + { + "c":[ + { + "u":{ + "a":"Version", + "c":6, + "s":"1.0.1" + } + }, + { + "u":{ + "a":"Version", + "c":9, + "s":"1.0.1" + } + } + ], + "s":{ + "v":{ + "s":"7" + }, + "i":"1637ffc5" + } + }, + { + "c":[ + { + "u":{ + "a":"Version", + "c":8, + "s":"0.9.9" + } + }, + { + "u":{ + "a":"Version", + "c":7, + "s":"0.9.9" + } + } + ], + "s":{ + "v":{ + "s":"8" + }, + "i":"b084ddd6" + } + }, + { + "c":[ + { + "u":{ + "a":"Number", + "c":10, + "d":1 + } + }, + { + "u":{ + "a":"Number", + "c":11, + "d":1 + } + } + ], + "s":{ + "v":{ + "s":"9" + }, + "i":"d1d537a6" + } + }, + { + "c":[ + { + "u":{ + "a":"Number", + "c":12, + "d":1.1 + } + }, + { + "u":{ + "a":"Number", + "c":15, + "d":1.1 + } + } + ], + "s":{ + "v":{ + "s":"10" + }, + "i":"52c846d0" + } + }, + { + "c":[ + { + "u":{ + "a":"Number", + "c":14, + "d":0.9 + } + }, + { + "u":{ + "a":"Number", + "c":13, + "d":0.9 + } + } + ], + "s":{ + "v":{ + "s":"11" + }, + "i":"c91ffb7c" + } + }, + { + "c":[ + { + "u":{ + "a":"Date", + "c":18, + "d":1693497600 + } + }, + { + "u":{ + "a":"Date", + "c":19, + "d":1693497600 + } + } + ], + "s":{ + "v":{ + "s":"12" + }, + "i":"c12182ef" + } + }, + { + "c":[ + { + "u":{ + "a":"Country", + "c":26, + "l":[ + "5a85699e7343a36d89ee75dca859f7a73cb6be89182095bffb021d1d78de046c" + ] + } + }, + { + "u":{ + "a":"Country", + "c":27, + "l":[ + "5a85699e7343a36d89ee75dca859f7a73cb6be89182095bffb021d1d78de046c" + ] + } + } + ], + "s":{ + "v":{ + "s":"13h" + }, + "i":"a16b1a17" + } + }, + { + "c":[ + { + "u":{ + "a":"Country", + "c":34, + "l":[ + "USA" + ] + } + }, + { + "u":{ + "a":"Country", + "c":35, + "l":[ + "USA" + ] + } + } + ], + "s":{ + "v":{ + "s":"13c" + }, + "i":"1a17d1b3" + } + } + ], + "v":{ + "s":"default" + }, + "i":"9ff25f81" + }, + "arrayContainsCaseCheckDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":26, + "l":[ + "00083f86e0f648b23f6721d43033bbef14378266fbf7de8a6760cc2ad237e9f3" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"5d80eff1" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"ce055a38" + }, + "arrayContainsDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":26, + "l":[ + "c2d5024661bc0e13f769cbb28bbfab7b78dac88b7876f007020f2e7cd47b1114" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"147fdd01" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"5f573f9c" + }, + "arrayDoesNotContainCaseCheckDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":27, + "l":[ + "14748140c64ecab48c7fd13f03811fe6390c8f578c99df96cf36fc2c6152f660" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"d4ad5730" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"df4915fd" + }, + "arrayDoesNotContainDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":27, + "l":[ + "bc0c24462c5098e434c63c7fcc6343af5000a4e7affd309f365edd4ccb7f428b" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"c2161ac9" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"41910880" + }, + "boolTrueIn202304":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":19, + "d":1680307200 + } + }, + { + "u":{ + "a":"Custom1", + "c":18, + "d":1682899200 + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"6948d7cd" + } + } + ], + "v":{ + "b":false + }, + "i":"ae2a09bd" + }, + "countryPercentageAttribute":{ + "t":1, + "a":"Country", + "p":[ + { + "p":50, + "v":{ + "s":"Falcon" + }, + "i":"2b05fd81" + }, + { + "p":50, + "v":{ + "s":"Horse" + }, + "i":"e28b6a82" + } + ], + "v":{ + "s":"Chicken" + }, + "i":"29bb6bbb" + }, + "customPercentageAttribute":{ + "t":1, + "a":"Custom1", + "p":[ + { + "p":50, + "v":{ + "s":"Falcon" + }, + "i":"3715712d" + }, + { + "p":50, + "v":{ + "s":"Horse" + }, + "i":"7b3542d5" + } + ], + "v":{ + "s":"Chicken" + }, + "i":"50466fb6" + }, + "missingPercentageAttribute":{ + "t":1, + "a":"NotFound", + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "14_902a42101e8b77851c98456b383fd959ed0f5aed5b919b4a623c8c756cf0c3ab" + ] + } + } + ], + "p":[ + { + "p":50, + "v":{ + "s":"Falcon" + }, + "i":"4b7d88ba" + }, + { + "p":50, + "v":{ + "s":"Horse" + }, + "i":"a1c2c9a9" + } + ] + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "14_902a42101e8b77851c98456b383fd959ed0f5aed5b919b4a623c8c756cf0c3ab" + ] + } + } + ], + "s":{ + "v":{ + "s":"NotFound" + }, + "i":"8aa042fe" + } + } + ], + "v":{ + "s":"Chicken" + }, + "i":"e5107172" + }, + "stringArrayContainsAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":34, + "l":[ + "read", + "write", + "execute" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"9ddb8a37" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"0d45ab4b" + }, + "stringArrayContainsAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":26, + "l":[ + "090415ce6b462a2152e06d68aeb7c452a564d19a262eb959c510636a189e105d", + "0aa50b49ca02a59cf507856d88ab25b76a7e69e553d25e67254359d8bbb8b1d6", + "1aa91982f7ccae1943f05ac437d635b3261e3ce06aba846dd2ecc6332e4675c2" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"aa03b1ff" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"203317f5" + }, + "stringArrayNotContainsAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":35, + "l":[ + "read", + "write", + "execute" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"15c865df" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"6df210da" + }, + "stringArrayNotContainsAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":27, + "l":[ + "b645a36d4d2e24a0612296ded074eace87ce10a0da21c5cb2ac9a6dccbe79cc5", + "552a92975423ea255384f48a6387be172c05ac72238f90050cdfe27cc4659cfd", + "4c00d4171202dbeb35830b1df8e47185968351c771bb2f633ea50ac1c049e016" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"259816ba" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"60b961b0" + }, + "stringContainsAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@verizon.com", + "@verizon.net", + "@aol.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"09af657f" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"063bcf39" + }, + "stringDoseNotEqualDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":21, + "s":"3cb496a1c3844215c784116ce9e91c143860d7ef18f7fadadcc901b8df5d235c" + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"8e423808" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"1835a09a" + }, + "stringEndsWithAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":32, + "l":[ + "@verizon.com", + "@verizon.net", + "@aol.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"33d35402" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"31976ec3" + }, + "stringEndsWithAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "12_781f5f9b054a14721a835fcdd2d03ac6d45d99eb55b387bf5938904c8f65aa35", + "12_d15f199cfbd96ef1c6f7683e66d5e0f85c9c591ce377289abb2e785fc71bbdf1", + "8_6ee234c513b7518bd705cc1049576c1e757a201200ff26a6a3a91821f954c6cb" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"7231ddf8" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"de17fd2a" + }, + "stringEndsWithDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "14_236ec291c9b54fad3373a0b7e0e465b33459198fd1027838c101516ad8ae1b39" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"d7a00741" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"45b7d922" + }, + "stringEqualsCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":28, + "s":"a@configcat.com" + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"087e01dd" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"89785ab3" + }, + "stringEqualsDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":20, + "s":"86e6b430939acac093f3d8c48be10798896f0abf3b96e2e39080acedd925d887" + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"703c31ed" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"adc0b01c" + }, + "stringNotContainsAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":3, + "l":[ + "@verizon.com", + "@verizon.net", + "@aol.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"49627b36" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"36848b03" + }, + "stringNotEndsWithAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":33, + "l":[ + "@verizon.com", + "@verizon.net", + "@aol.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"886ced9d" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"864b6202" + }, + "stringNotEndsWithAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":25, + "l":[ + "12_295e66dc483034349bc6cffd14190ccff949ac1f34df5fbf01437b8162719b98", + "12_11933417446186b92f63db5931a275f156485d0aef5ee8e265afb350921bd0b1", + "8_aa181c1d9462e3916cb05669e2c408541edf03dad19a9655b340eb32ddd4d060" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"6eb0ec3a" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"7020bcd6" + }, + "stringNotEndsWithDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":25, + "l":[ + "14_141d3f398885dd06d6c14e6629602125d8cb0dddf2ef85896f682c74abb4bc28" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"d37b6f18" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"91ba1bcb" + }, + "stringNotEqualsCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":29, + "s":"b@configcat.com" + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"3ace20fb" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"09c9725f" + }, + "stringNotStartsWithAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":31, + "l":[ + "u@", + "a@", + "b@" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"3717f2ca" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"1d661433" + }, + "stringNotStartsWithAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":23, + "l":[ + "2_b0ab1063bba4431f95710a8bd9c9e2bcd1cebc09fe6803cf87ee501d045e8ee0", + "2_9206a0a6b987410488c8020650788c597509af05aa5327f92cae1c999c401c34", + "2_d8f5c200e2c54b0f84f7df024576f799f484d0e258c6058142a4993daa5e2998" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"b5ba025e" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"c35929e3" + }, + "stringNotStartsWithDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":23, + "l":[ + "1_061a38ed8d5955d90b88d1b1949512a45032390a2581f3c7e36597f7459a48c7" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"72c4e1ac" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"2b16da78" + }, + "stringStartsWithAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":30, + "l":[ + "u@", + "a@", + "b@" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"9e55f5cf" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"e170a185" + }, + "stringStartsWithAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":22, + "l":[ + "2_52f47d1a193071a304e41812055cc1282d90287e5c993ae159fa08fb1ccd3656", + "2_6e5ae1bb586660088d330ac106bbf6b7f2b43be1ca70dbb766f203b76bc84049", + "2_3edba8b738135ad32e85d8695acb8d8511ac67d83f50da55abd9ba469da88efe" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"1d9b7603" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"dd5b3211" + }, + "stringStartsWithDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":22, + "l":[ + "1_55e0b0566d89dc0fda2323efcfb958c782a4648513bdcc4dc84b044fd34230dd" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"3b409872" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"3659b0fe" + } + } +} +""" +} diff --git a/src/commonTest/kotlin/com/configcat/integration/matrix/Matrix.kt b/src/commonTest/kotlin/com/configcat/integration/matrix/Matrix.kt index ec94287c..5cf3732c 100644 --- a/src/commonTest/kotlin/com/configcat/integration/matrix/Matrix.kt +++ b/src/commonTest/kotlin/com/configcat/integration/matrix/Matrix.kt @@ -1,7 +1,7 @@ package com.configcat.integration.matrix object Matrix : DataMatrix { - override val sdkKey = "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A" + override val sdkKey: String = "configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/AG6C1ngVb0CvM07un6JisQ" override val data = """Identifier;Email;Country;Custom1;bool30TrueAdvancedRules;boolDefaultFalse;boolDefaultTrue;double25Pi25E25Gr25Zero;doubleDefaultPi;integer25One25Two25Three25FourAdvancedRules;integerDefaultOne;string25Cat25Dog25Falcon25Horse;string25Cat25Dog25Falcon25HorseAdvancedRules;string75Cat0Dog25Falcon0Horse;stringContainsDogDefaultCat;stringDefaultCat;stringIsInDogDefaultCat;stringIsNotInDogDefaultCat;stringNotContainsDogDefaultCat ##null##;;;;True;False;True;-1;3.1415;-1;1;Chicken;Chicken;Chicken;Cat;Cat;Cat;Cat;Cat @@ -1017,5 +1017,598 @@ garland@outlook.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Hor 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""" override val remoteJson = - """{"p":{"u":"https://cdn-global.configcat.com","r":0},"f":{"stringDefaultCat":{"v":"Cat","t":1,"i":"7a0be518","p":[],"r":[]},"stringIsInDogDefaultCat":{"v":"Cat","t":1,"i":"83372510","p":[],"r":[{"o":0,"a":"Email","t":0,"c":"a@configcat.com, b@configcat.com","v":"Dog","i":"5b64d9b4"},{"o":1,"a":"Custom1","t":0,"c":"admin","v":"Dog","i":"5b64d9b4"}]},"stringIsNotInDogDefaultCat":{"v":"Cat","t":1,"i":"2459598d","p":[],"r":[{"o":0,"a":"Email","t":1,"c":"a@configcat.com,b@configcat.com","v":"Dog","i":"6ada5ff2"}]},"stringContainsDogDefaultCat":{"v":"Cat","t":1,"i":"ce564c3a","p":[],"r":[{"o":0,"a":"Email","t":2,"c":"@configcat.com","v":"Dog","i":"d0cd8f06"}]},"stringNotContainsDogDefaultCat":{"v":"Cat","t":1,"i":"44ab483a","p":[],"r":[{"o":0,"a":"Email","t":3,"c":"@configcat.com","v":"Dog","i":"f7f8f43d"}]},"string25Cat25Dog25Falcon25Horse":{"v":"Chicken","t":1,"i":"2588a3e6","p":[{"o":0,"v":"Cat","p":25,"i":"d227b334"},{"o":1,"v":"Dog","p":25,"i":"622f5d07"},{"o":2,"v":"Falcon","p":25,"i":"0ff32bab"},{"o":3,"v":"Horse","p":25,"i":"6c597441"}],"r":[]},"string75Cat0Dog25Falcon0Horse":{"v":"Chicken","t":1,"i":"aa65b5ce","p":[{"o":0,"v":"Cat","p":75,"i":"93f5a1c0"},{"o":1,"v":"Dog","p":0,"i":"b8f49554"},{"o":2,"v":"Falcon","p":25,"i":"7beaf504"},{"o":3,"v":"Horse","p":0,"i":"30ee31af"}],"r":[]},"string25Cat25Dog25Falcon25HorseAdvancedRules":{"v":"Chicken","t":1,"i":"8250ef5a","p":[{"o":0,"v":"Cat","p":25,"i":"83461b47"},{"o":1,"v":"Dog","p":25,"i":"4f026fbc"},{"o":2,"v":"Falcon","p":25,"i":"392a4d59"},{"o":3,"v":"Horse","p":25,"i":"bb66b1f3"}],"r":[{"o":0,"a":"Country","t":0,"c":"Hungary, United Kingdom","v":"Dolphin","i":"3accb1d0"},{"o":1,"a":"Custom1","t":2,"c":"admi","v":"Lion","i":"e95ebf10"},{"o":2,"a":"Email","t":2,"c":"@configcat.com","v":"Kitten","i":"88243650"}]},"boolDefaultTrue":{"v":true,"i":"09513143","t":0,"p":[],"r":[]},"boolDefaultFalse":{"v":false,"i":"489a16d2","t":0,"p":[],"r":[]},"bool30TrueAdvancedRules":{"v":true,"i":"607147d5","t":0,"p":[{"o":0,"v":true,"p":30,"i":"607147d5"},{"o":1,"v":false,"p":70,"i":"385d9803"}],"r":[{"o":0,"a":"Email","t":0,"c":"a@configcat.com, b@configcat.com","v":false,"i":"385d9803"},{"o":1,"a":"Country","t":2,"c":"United","v":false,"i":"385d9803"}]},"integer25One25Two25Three25FourAdvancedRules":{"v":-1,"i":"ce3c4f5a","t":2,"p":[{"o":0,"v":1,"p":25,"i":"11634414"},{"o":1,"v":2,"p":25,"i":"5530655d"},{"o":2,"v":3,"p":25,"i":"2ad19a52"},{"o":3,"v":4,"p":25,"i":"41b30851"}],"r":[{"o":0,"a":"Email","t":2,"c":"@configcat.com","v":5,"i":"58136ba2"}]},"integerDefaultOne":{"v":1,"i":"faadbf54","t":2,"p":[],"r":[]},"doubleDefaultPi":{"v":3.1415,"i":"5af8acc7","t":3,"p":[],"r":[]},"double25Pi25E25Gr25Zero":{"v":-1.0,"i":"9503a1de","t":3,"p":[{"o":0,"v":3.1415,"p":25,"i":"6d75b4d3"},{"o":1,"v":2.7182,"p":25,"i":"183ee713"},{"o":2,"v":1.61803,"p":25,"i":"01eb6326"},{"o":3,"v":0.0,"p":25,"i":"64c434ff"}],"r":[{"o":0,"a":"Email","t":2,"c":"@configcat.com","v":5.561,"i":"3f7826de"}]},"keySampleText":{"v":"Cat","t":1,"i":"69ef126c","p":[{"o":0,"v":"Falcon","p":50,"i":"baff2362"},{"o":1,"v":"Horse","p":50,"i":"dab78ba5"}],"r":[{"o":0,"a":"Country","t":0,"c":"Hungary,Bahamas","v":"Dog","i":"9fa0e57e"},{"o":1,"a":"SubscriptionType","t":0,"c":"unlimited","v":"Lion","i":"2be6b03f"}]}}}""" + """ +{ + "p":{ + "u":"https://cdn-global.configcat.com", + "r":0, + "s":"2mTrfJifhGjGijMMwV06xC\u002Bg\u002Biex16oITb2wSMUgJSI=" + }, + "f":{ + "bool30TrueAdvancedRules":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":0, + "l":[ + "a@configcat.com", + "b@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "b":false + }, + "i":"385d9803" + } + }, + { + "c":[ + { + "u":{ + "a":"Country", + "c":2, + "l":[ + "United" + ] + } + } + ], + "s":{ + "v":{ + "b":false + }, + "i":"385d9803" + } + } + ], + "p":[ + { + "p":30, + "v":{ + "b":true + }, + "i":"607147d5" + }, + { + "p":70, + "v":{ + "b":false + }, + "i":"385d9803" + } + ], + "v":{ + "b":true + }, + "i":"607147d5" + }, + "boolDefaultFalse":{ + "t":0, + "v":{ + "b":false + }, + "i":"489a16d2" + }, + "boolDefaultTrue":{ + "t":0, + "v":{ + "b":true + }, + "i":"09513143" + }, + "double25Pi25E25Gr25Zero":{ + "t":3, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "d":5.561 + }, + "i":"3f7826de" + } + } + ], + "p":[ + { + "p":25, + "v":{ + "d":3.1415 + }, + "i":"6d75b4d3" + }, + { + "p":25, + "v":{ + "d":2.7182 + }, + "i":"183ee713" + }, + { + "p":25, + "v":{ + "d":1.61803 + }, + "i":"01eb6326" + }, + { + "p":25, + "v":{ + "d":0 + }, + "i":"64c434ff" + } + ], + "v":{ + "d":-1 + }, + "i":"9503a1de" + }, + "doubleDefaultPi":{ + "t":3, + "v":{ + "d":3.1415 + }, + "i":"5af8acc7" + }, + "integer25One25Two25Three25FourAdvancedRules":{ + "t":2, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "i":5 + }, + "i":"58136ba2" + } + } + ], + "p":[ + { + "p":25, + "v":{ + "i":1 + }, + "i":"11634414" + }, + { + "p":25, + "v":{ + "i":2 + }, + "i":"5530655d" + }, + { + "p":25, + "v":{ + "i":3 + }, + "i":"2ad19a52" + }, + { + "p":25, + "v":{ + "i":4 + }, + "i":"41b30851" + } + ], + "v":{ + "i":-1 + }, + "i":"ce3c4f5a" + }, + "integerDefaultOne":{ + "t":2, + "v":{ + "i":1 + }, + "i":"faadbf54" + }, + "keySampleText":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Country", + "c":0, + "l":[ + "Hungary", + "Bahamas" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"9fa0e57e" + } + }, + { + "c":[ + { + "u":{ + "a":"SubscriptionType", + "c":0, + "l":[ + "unlimited" + ] + } + } + ], + "s":{ + "v":{ + "s":"Lion" + }, + "i":"2be6b03f" + } + } + ], + "p":[ + { + "p":50, + "v":{ + "s":"Falcon" + }, + "i":"baff2362" + }, + { + "p":50, + "v":{ + "s":"Horse" + }, + "i":"dab78ba5" + } + ], + "v":{ + "s":"Cat" + }, + "i":"69ef126c" + }, + "string25Cat25Dog25Falcon25Horse":{ + "t":1, + "p":[ + { + "p":25, + "v":{ + "s":"Cat" + }, + "i":"d227b334" + }, + { + "p":25, + "v":{ + "s":"Dog" + }, + "i":"622f5d07" + }, + { + "p":25, + "v":{ + "s":"Falcon" + }, + "i":"0ff32bab" + }, + { + "p":25, + "v":{ + "s":"Horse" + }, + "i":"6c597441" + } + ], + "v":{ + "s":"Chicken" + }, + "i":"2588a3e6" + }, + "string25Cat25Dog25Falcon25HorseAdvancedRules":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Country", + "c":0, + "l":[ + "Hungary", + "United Kingdom" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dolphin" + }, + "i":"3accb1d0" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":2, + "l":[ + "admi" + ] + } + } + ], + "s":{ + "v":{ + "s":"Lion" + }, + "i":"e95ebf10" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Kitten" + }, + "i":"88243650" + } + } + ], + "p":[ + { + "p":25, + "v":{ + "s":"Cat" + }, + "i":"83461b47" + }, + { + "p":25, + "v":{ + "s":"Dog" + }, + "i":"4f026fbc" + }, + { + "p":25, + "v":{ + "s":"Falcon" + }, + "i":"392a4d59" + }, + { + "p":25, + "v":{ + "s":"Horse" + }, + "i":"bb66b1f3" + } + ], + "v":{ + "s":"Chicken" + }, + "i":"8250ef5a" + }, + "string75Cat0Dog25Falcon0Horse":{ + "t":1, + "p":[ + { + "p":75, + "v":{ + "s":"Cat" + }, + "i":"93f5a1c0" + }, + { + "p":0, + "v":{ + "s":"Dog" + }, + "i":"b8f49554" + }, + { + "p":25, + "v":{ + "s":"Falcon" + }, + "i":"7beaf504" + }, + { + "p":0, + "v":{ + "s":"Horse" + }, + "i":"30ee31af" + } + ], + "v":{ + "s":"Chicken" + }, + "i":"aa65b5ce" + }, + "stringContainsDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"d0cd8f06" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"ce564c3a" + }, + "stringDefaultCat":{ + "t":1, + "v":{ + "s":"Cat" + }, + "i":"7a0be518" + }, + "stringIsInDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":0, + "l":[ + "a@configcat.com", + "b@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"5b64d9b4" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":0, + "l":[ + "admin" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"5b64d9b4" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"83372510" + }, + "stringIsNotInDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":1, + "l":[ + "a@configcat.com", + "b@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"6ada5ff2" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"2459598d" + }, + "stringNotContainsDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":3, + "l":[ + "@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"f7f8f43d" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"44ab483a" + } + } +} +""" } diff --git a/src/commonTest/kotlin/com/configcat/integration/matrix/NumberMatrix.kt b/src/commonTest/kotlin/com/configcat/integration/matrix/NumberMatrix.kt index 227ae5e3..649f097e 100644 --- a/src/commonTest/kotlin/com/configcat/integration/matrix/NumberMatrix.kt +++ b/src/commonTest/kotlin/com/configcat/integration/matrix/NumberMatrix.kt @@ -1,7 +1,7 @@ package com.configcat.integration.matrix object NumberMatrix : DataMatrix { - override val sdkKey = "PKDVCLf-Hq-h-kCzMp-L7Q/uGyK3q9_ckmdxRyI7vjwCw" + override val sdkKey: String = "configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw" override val data = """Identifier;Email;Country;Custom1;numberWithPercentage;number ##null##;;;;Default;Default id1;;;0;<2.1;<>5 @@ -29,5 +29,166 @@ id17;;;4,0;<>4.2;<>5 id18;;;4.2;80%;<>5 id19;;;4,2;20%;<>5""" override val remoteJson = - """{"p":{"u":"https://cdn-global.configcat.com","r":0},"f":{"numberWithPercentage":{"v":"Default","t":1,"i":"642bbb26","p":[{"o":0,"v":"80%","p":80,"i":"ad5f05a7"},{"o":1,"v":"20%","p":20,"i":"786b696f"}],"r":[{"o":0,"a":"Custom1","t":10,"c":"sajt","v":"=sajt","i":"216987c8"},{"o":1,"a":"Custom1","t":12,"c":"2.1","v":"<2.1","i":"a900bc23"},{"o":2,"a":"Custom1","t":13,"c":"2,1","v":"<=2,1","i":"2c85f73d"},{"o":3,"a":"Custom1","t":10,"c":"3.5","v":"=3.5","i":"ae86baf5"},{"o":4,"a":"Custom1","t":14,"c":"5","v":">5","i":"c6924001"},{"o":5,"a":"Custom1","t":15,"c":"5","v":">=5","i":"8090543a"},{"o":6,"a":"Custom1","t":11,"c":"4.2","v":"<>4.2","i":"2691fade"}]},"number":{"v":"Default","t":1,"i":"5ced27a9","p":[],"r":[{"o":0,"a":"Custom1","t":11,"c":"5","v":"<>5","i":"a41938c5"}]}}}""" + """{ + "p":{ + "u":"https://cdn-global.configcat.com", + "r":0, + "s":"SLNaHb27PQQMYKE2Hk555sybLOakz7wv00QzZcWfPFo=" + }, + "f":{ + "number":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":11, + "d":5 + } + } + ], + "s":{ + "v":{ + "s":"\u003C\u003E5" + }, + "i":"a41938c5" + } + } + ], + "v":{ + "s":"Default" + }, + "i":"5ced27a9" + }, + "numberWithPercentage":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":12, + "d":2.1 + } + } + ], + "s":{ + "v":{ + "s":"\u003C2.1" + }, + "i":"a900bc23" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":13, + "d":2.1 + } + } + ], + "s":{ + "v":{ + "s":"\u003C=2,1" + }, + "i":"2c85f73d" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":10, + "d":3.5 + } + } + ], + "s":{ + "v":{ + "s":"=3.5" + }, + "i":"ae86baf5" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":14, + "d":5 + } + } + ], + "s":{ + "v":{ + "s":"\u003E5" + }, + "i":"c6924001" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":15, + "d":5 + } + } + ], + "s":{ + "v":{ + "s":"\u003E=5" + }, + "i":"8090543a" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":11, + "d":4.2 + } + } + ], + "s":{ + "v":{ + "s":"\u003C\u003E4.2" + }, + "i":"2691fade" + } + } + ], + "p":[ + { + "p":80, + "v":{ + "s":"80%" + }, + "i":"ad5f05a7" + }, + { + "p":20, + "v":{ + "s":"20%" + }, + "i":"786b696f" + } + ], + "v":{ + "s":"Default" + }, + "i":"642bbb26" + } + } +}""" } diff --git a/src/commonTest/kotlin/com/configcat/integration/matrix/PrerequisiteFlagMatrix.kt b/src/commonTest/kotlin/com/configcat/integration/matrix/PrerequisiteFlagMatrix.kt new file mode 100644 index 00000000..8d89a856 --- /dev/null +++ b/src/commonTest/kotlin/com/configcat/integration/matrix/PrerequisiteFlagMatrix.kt @@ -0,0 +1,533 @@ +package com.configcat.integration.matrix + +object PrerequisiteFlagMatrix : DataMatrix { + override val sdkKey: String = "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/JoGwdqJZQ0K2xDy7LnbyOg" + override val data = + """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""" + override val remoteJson = + """{ + "p":{ + "u":"https://cdn-global.configcat.com", + "r":0, + "s":"PBMv8zBDvXO9ZObbLwsP5TQOsgn8aOv1K3+xPFJCoAU=" + }, + "f":{ + "boolDependsOnBool":{ + "t":0, + "r":[ + { + "c":[ + { + "p":{ + "f":"mainBoolFlag", + "c":0, + "v":{ + "b":true + } + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"8dc94c1d" + } + } + ], + "v":{ + "b":false + }, + "i":"d6194760" + }, + "boolDependsOnBoolDependsOnBool":{ + "t":0, + "r":[ + { + "c":[ + { + "p":{ + "f":"boolDependsOnBool", + "c":0, + "v":{ + "b":true + } + } + } + ], + "s":{ + "v":{ + "b":false + }, + "i":"d6870486" + } + } + ], + "v":{ + "b":true + }, + "i":"cd4c95e7" + }, + "boolDependsOnBoolInverse":{ + "t":0, + "r":[ + { + "c":[ + { + "p":{ + "f":"mainBoolFlagInverse", + "c":1, + "v":{ + "b":true + } + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"3c09bff0" + } + } + ], + "v":{ + "b":false + }, + "i":"cecbc501" + }, + "doubleDependsOnBool":{ + "t":3, + "r":[ + { + "c":[ + { + "p":{ + "f":"mainBoolFlag", + "c":0, + "v":{ + "b":true + } + } + } + ], + "s":{ + "v":{ + "d":1.1 + }, + "i":"271fd003" + } + } + ], + "v":{ + "d":3.14 + }, + "i":"718aae2b" + }, + "intDependsOnBool":{ + "t":2, + "r":[ + { + "c":[ + { + "p":{ + "f":"mainBoolFlag", + "c":0, + "v":{ + "b":true + } + } + } + ], + "s":{ + "v":{ + "i":1 + }, + "i":"d2dda649" + } + } + ], + "v":{ + "i":42 + }, + "i":"43ec49a8" + }, + "mainBoolFlag":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "21_32abe94b0866402b226383eb666a98312dc898119e2a9241ffbfcc114eb6a57b" + ] + } + } + ], + "s":{ + "v":{ + "b":false + }, + "i":"e842ea6f" + } + } + ], + "v":{ + "b":true + }, + "i":"8a68b064" + }, + "mainBoolFlagEmpty":{ + "t":0, + "v":{ + "b":true + }, + "i":"f3295d43" + }, + "mainBoolFlagInverse":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "21_69627ce988f31d14807ed75022d5325645914dadc3bfe7cdc1b6dbeca8763b67" + ] + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"28c65f1f" + } + } + ], + "v":{ + "b":false + }, + "i":"d70e47a7" + }, + "mainDoubleFlag":{ + "t":3, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "21_4cb521a31b1b604875ec3c7c90553a7cb692434f9aee8a318215f9bf1165f0e3" + ] + } + } + ], + "s":{ + "v":{ + "d":0.1 + }, + "i":"a67947ed" + } + } + ], + "v":{ + "d":3.14 + }, + "i":"beb3acc7" + }, + "mainIntFlag":{ + "t":2, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "21_0ad4d095ab7ae197936c7dde2a53e55b2df616c0845c9b216ade6f14b2a4cf3d" + ] + } + } + ], + "s":{ + "v":{ + "i":2 + }, + "i":"67e14078" + } + } + ], + "v":{ + "i":42 + }, + "i":"a7490aca" + }, + "mainStringFlag":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "21_78d8c5a677414bd170650ec60b51e9325663ef8447b280862ec52be49cca7b0f" + ] + } + } + ], + "s":{ + "v":{ + "s":"private" + }, + "i":"51b57fb0" + } + } + ], + "v":{ + "s":"public" + }, + "i":"24c96275" + }, + "stringDependsOnBool":{ + "t":1, + "r":[ + { + "c":[ + { + "p":{ + "f":"mainBoolFlag", + "c":0, + "v":{ + "b":true + } + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"fc8daf80" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"d53a2b42" + }, + "stringDependsOnDouble":{ + "t":1, + "r":[ + { + "c":[ + { + "p":{ + "f":"mainDoubleFlag", + "c":0, + "v":{ + "d":0.1 + } + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"84fc7ed9" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"9cc8fd8f" + }, + "stringDependsOnDoubleIntValue":{ + "t":1, + "r":[ + { + "c":[ + { + "p":{ + "f":"mainDoubleFlag", + "c":0, + "v":{ + "d":0 + } + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"842c1d75" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"db7f56c8" + }, + "stringDependsOnEmptyBool":{ + "t":1, + "r":[ + { + "c":[ + { + "p":{ + "f":"mainBoolFlagEmpty", + "c":0, + "v":{ + "b":true + } + } + } + ], + "s":{ + "v":{ + "s":"EmptyOn" + }, + "i":"d5508c78" + } + } + ], + "v":{ + "s":"EmptyOff" + }, + "i":"8e0dbe88" + }, + "stringDependsOnInt":{ + "t":1, + "r":[ + { + "c":[ + { + "p":{ + "f":"mainIntFlag", + "c":0, + "v":{ + "i":2 + } + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"12531eec" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"e227d926" + }, + "stringDependsOnString":{ + "t":1, + "r":[ + { + "c":[ + { + "p":{ + "f":"mainStringFlag", + "c":0, + "v":{ + "s":"private" + } + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"426b6d4d" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"d36000e1" + }, + "stringDependsOnStringCaseCheck":{ + "t":1, + "r":[ + { + "c":[ + { + "p":{ + "f":"mainStringFlag", + "c":0, + "v":{ + "s":"Private" + } + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"87d24aed" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"ad94f385" + }, + "stringInverseDependsOnEmptyBool":{ + "t":1, + "r":[ + { + "c":[ + { + "p":{ + "f":"mainBoolFlagEmpty", + "c":1, + "v":{ + "b":true + } + } + } + ], + "s":{ + "v":{ + "s":"EmptyOff" + }, + "i":"b7c3efae" + } + } + ], + "v":{ + "s":"EmptyOn" + }, + "i":"f6b4b8a2" + } + } +} +""" +} diff --git a/src/commonTest/kotlin/com/configcat/integration/matrix/SegmentMatrix.kt b/src/commonTest/kotlin/com/configcat/integration/matrix/SegmentMatrix.kt new file mode 100644 index 00000000..349b06a5 --- /dev/null +++ b/src/commonTest/kotlin/com/configcat/integration/matrix/SegmentMatrix.kt @@ -0,0 +1,281 @@ +package com.configcat.integration.matrix + +object SegmentMatrix : DataMatrix { + override val sdkKey: String = "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/h99HYXWWNE2bH8eWyLAVMA" + override val data = + """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""" + override val remoteJson = + """{ + "p":{ + "u":"https://cdn-global.configcat.com", + "r":0, + "s":"AfP/HFxenWYu4mTtHLlrNSQTV6DIAVnqRoNiaF7fLGQ=" + }, + "s":[ + { + "n":"Beta Users", + "r":[ + { + "a":"Email", + "c":16, + "l":[ + "6cba762cb8633edb821b0d053b889078a7196dfeaff76bd7093db1405540fce0", + "41c849db4599b0ebcedecd6a64904401ffcd4d9c26137907cc0ef16525daa665" + ] + } + ] + }, + { + "n":"Developers", + "r":[ + { + "a":"Email", + "c":16, + "l":[ + "aae4b03d832f66cfe06b716e2af6ea063ce71408728814fe78fbfec8fbc20d76", + "2ebeb92c2192e27005ac0a954adc38a1986639976acf61c77a852b86200d67f4" + ] + } + ] + }, + { + "n":"Not Beta Users", + "r":[ + { + "a":"Email", + "c":17, + "l":[ + "15ac2079d47e7d0c94f2d85327b2c262014abbfbbcbb200c932a6bcce5933ea1", + "e24dd5274f7b82521e19b19f4e9fe5c586bfc9ceb09683d7e7ba948d3f8dc023" + ] + } + ] + }, + { + "n":"Not Developers", + "r":[ + { + "a":"Email", + "c":17, + "l":[ + "2c9f86d42b5e5206a3192bb66f086456ac2641affa1c3422deb4f36923711e13", + "c78bf0398d5e526fc63c2853107211fc66ec692cccd69c5610cf19a30fb8c828" + ] + } + ] + }, + { + "n":"Not States", + "r":[ + { + "a":"Country", + "c":3, + "l":[ + "States" + ] + } + ] + }, + { + "n":"United", + "r":[ + { + "a":"Country", + "c":2, + "l":[ + "United" + ] + } + ] + }, + { + "n":"Beta Users (cleartext)", + "r":[ + { + "a":"Email", + "c":0, + "l":[ + "jane@example.com", + "john@example.com" + ] + } + ] + }, + { + "n":"Not Beta Users (cleartext)", + "r":[ + { + "a":"Email", + "c":1, + "l":[ + "jane@example.com", + "john@example.com" + ] + } + ] + } + ], + "f":{ + "countrySegment":{ + "t":1, + "r":[ + { + "c":[ + { + "s":{ + "s":5, + "c":0 + } + }, + { + "s":{ + "s":4, + "c":0 + } + } + ], + "s":{ + "v":{ + "s":"A" + }, + "i":"9b7e6414" + } + } + ], + "v":{ + "s":"Z" + }, + "i":"f71b6d96" + }, + "developerAndBetaUserCleartextSegment":{ + "t":0, + "r":[ + { + "c":[ + { + "s":{ + "s":6, + "c":1 + } + }, + { + "s":{ + "s":1, + "c":0 + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"586d85a7" + } + } + ], + "v":{ + "b":false + }, + "i":"80c95c76" + }, + "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" + }, + "notDeveloperAndNotBetaUserCleartextSegment":{ + "t":0, + "r":[ + { + "c":[ + { + "s":{ + "s":3, + "c":1 + } + }, + { + "s":{ + "s":7, + "c":0 + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"46b767da" + } + } + ], + "v":{ + "b":false + }, + "i":"3af487b7" + }, + "notDeveloperAndNotBetaUserSegment":{ + "t":0, + "r":[ + { + "c":[ + { + "s":{ + "s":2, + "c":0 + } + }, + { + "s":{ + "s":3, + "c":1 + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"77081d42" + } + } + ], + "v":{ + "b":false + }, + "i":"a14eaf13" + } + } +}""" +} diff --git a/src/commonTest/kotlin/com/configcat/integration/matrix/SegmentsOldMatrix.kt b/src/commonTest/kotlin/com/configcat/integration/matrix/SegmentsOldMatrix.kt new file mode 100644 index 00000000..e01069aa --- /dev/null +++ b/src/commonTest/kotlin/com/configcat/integration/matrix/SegmentsOldMatrix.kt @@ -0,0 +1,276 @@ +package com.configcat.integration.matrix + +object SegmentsOldMatrix : DataMatrix { + override val sdkKey: String = "configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/y_ZB7o-Xb0Swxth-ZlMSeA" + override val data = + """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""" + override val remoteJson = + """{ + "p":{ + "u":"https://cdn-global.configcat.com", + "r":0, + "s":"cRPeZoMoPj/uTPnoDiZhJgUCI4WxJs1P65fNbntr8mY=" + }, + "s":[ + { + "n":"Beta users", + "r":[ + { + "a":"Email", + "c":16, + "l":[ + "b2cde2e3316460a5c0d546b1c31d21d5d1329c559beeeeb2f0adef94db24424b", + "e59c6e957433ec7599bb4a53124d2bee8aabf2f05dd3e89aadd993818783b903" + ] + } + ] + }, + { + "n":"Beta users (cleartext)", + "r":[ + { + "a":"Email", + "c":0, + "l":[ + "jane@example.com", + "john@example.com" + ] + } + ] + }, + { + "n":"Not Beta users", + "r":[ + { + "a":"Email", + "c":17, + "l":[ + "1b0852fa4eb5f205bbb19909c7c6ac6ca809831b14a83ccf3423c247ce429538", + "0fa7e84503164cac123153185e9522af87c178e00e4d2b16c2213e526372fdc3" + ] + } + ] + }, + { + "n":"Not Beta users (cleartext)", + "r":[ + { + "a":"Email", + "c":1, + "l":[ + "jane@example.com", + "john@example.com" + ] + } + ] + } + ], + "f":{ + "featureWithNegatedSegmentTargeting":{ + "t":0, + "r":[ + { + "c":[ + { + "s":{ + "s":0, + "c":1 + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"772939a0" + } + } + ], + "v":{ + "b":false + }, + "i":"3c0be020" + }, + "featureWithNegatedSegmentTargetingCleartext":{ + "t":0, + "r":[ + { + "c":[ + { + "s":{ + "s":1, + "c":1 + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"0fc9b378" + } + } + ], + "v":{ + "b":false + }, + "i":"6c5f81e3" + }, + "featureWithNegatedSegmentTargetingInverse":{ + "t":0, + "r":[ + { + "c":[ + { + "s":{ + "s":2, + "c":1 + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"145a1eb0" + } + } + ], + "v":{ + "b":false + }, + "i":"e9c52981" + }, + "featureWithNegatedSegmentTargetingInverseCleartext":{ + "t":0, + "r":[ + { + "c":[ + { + "s":{ + "s":3, + "c":1 + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"4898b966" + } + } + ], + "v":{ + "b":false + }, + "i":"fa8f80d5" + }, + "featureWithSegmentTargeting":{ + "t":0, + "r":[ + { + "c":[ + { + "s":{ + "s":0, + "c":0 + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"a49f6150" + } + } + ], + "v":{ + "b":false + }, + "i":"cfe41874" + }, + "featureWithSegmentTargetingCleartext":{ + "t":0, + "r":[ + { + "c":[ + { + "s":{ + "s":1, + "c":0 + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"d03ed88c" + } + } + ], + "v":{ + "b":false + }, + "i":"89fac05a" + }, + "featureWithSegmentTargetingInverse":{ + "t":0, + "r":[ + { + "c":[ + { + "s":{ + "s":2, + "c":0 + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"cf444ba3" + } + } + ], + "v":{ + "b":false + }, + "i":"2ddaee84" + }, + "featureWithSegmentTargetingInverseCleartext":{ + "t":0, + "r":[ + { + "c":[ + { + "s":{ + "s":3, + "c":0 + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"a78fc410" + } + } + ], + "v":{ + "b":false + }, + "i":"6a3224de" + } + } +}""" +} diff --git a/src/commonTest/kotlin/com/configcat/integration/matrix/SemanticMatrix.kt b/src/commonTest/kotlin/com/configcat/integration/matrix/SemanticMatrix.kt index d6043962..13d41f25 100644 --- a/src/commonTest/kotlin/com/configcat/integration/matrix/SemanticMatrix.kt +++ b/src/commonTest/kotlin/com/configcat/integration/matrix/SemanticMatrix.kt @@ -1,7 +1,7 @@ package com.configcat.integration.matrix object SemanticMatrix : DataMatrix { - override val sdkKey = "PKDVCLf-Hq-h-kCzMp-L7Q/BAr3KgLTP0ObzKnBTo5nhA" + override val sdkKey: String = "configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/iV8vH2MBakKxkFZylxHmTg" override val data = """Identifier;Email;Country;Custom1;isOneOf;isOneOfWithPercentage;isNotOneOf;isNotOneOfWithPercentage;lessThanWithPercentage;relations ##null##;;;;Default;Default;Default;Default;Default;Default @@ -40,5 +40,495 @@ 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, 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""" override val remoteJson = - """{"p":{"u":"https://cdn-global.configcat.com","r":0},"f":{"isOneOf":{"v":"Default","t":1,"i":"c4ec4d53","p":[],"r":[{"o":0,"a":"Custom1","t":4,"c":"1.0.0, 2","v":"Is one of (1.0.0, 2)","i":"1e934047"},{"o":1,"a":"Custom1","t":4,"c":"1.0.0","v":"Is one of (1.0.0)","i":"44342254"},{"o":2,"a":"Custom1","t":4,"c":" , 2.0.1, 2.0.2, ","v":"Is one of ( , 2.0.1, 2.0.2, )","i":"90e3ef46"},{"o":3,"a":"Custom1","t":4,"c":"3......","v":"Is one of (3......)","i":"59523971"},{"o":4,"a":"Custom1","t":4,"c":"3....","v":"Is one of (3...)","i":"2de217a1"},{"o":5,"a":"Custom1","t":4,"c":"3..0","v":"Is one of (3..0)","i":"bf943c79"},{"o":6,"a":"Custom1","t":4,"c":"3.0","v":"Is one of (3.0)","i":"3a6a8077"},{"o":7,"a":"Custom1","t":4,"c":"3.0.","v":"Is one of (3.0.)","i":"44f25fed"},{"o":8,"a":"Custom1","t":4,"c":"3.0.0","v":"Is one of (3.0.0)","i":"e77f5306"}]},"isOneOfWithPercentage":{"v":"Default","t":1,"i":"a94ff896","p":[{"o":0,"v":"20%","p":20,"i":"e25dba31"},{"o":1,"v":"80%","p":80,"i":"8c70c181"}],"r":[{"o":0,"a":"Custom1","t":4,"c":"1.0.0","v":"is one of (1.0.0)","i":"0ac4afc1"}]},"isNotOneOf":{"v":"Default","t":1,"i":"f79b763d","p":[],"r":[{"o":0,"a":"Custom1","t":5,"c":"1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, ","v":"Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, )","i":"a8d5f278"},{"o":1,"a":"Custom1","t":5,"c":"1.0.0, 3.0.1","v":"Is not one of (1.0.0, 3.0.1)","i":"54ac757f"}]},"isNotOneOfWithPercentage":{"v":"Default","t":1,"i":"b9614bad","p":[{"o":0,"v":"20%","p":20,"i":"68f652f0"},{"o":1,"v":"80%","p":80,"i":"b8d926e0"}],"r":[{"o":0,"a":"Custom1","t":5,"c":"1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, ","v":"Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, )","i":"9bf9e66f"},{"o":1,"a":"Custom1","t":5,"c":"1.0.0, 3.0.1","v":"Is not one of (1.0.0, 3.0.1)","i":"bfc1a544"}]},"lessThanWithPercentage":{"v":"Default","t":1,"i":"0081c525","p":[{"o":0,"v":"20%","p":20,"i":"3b1fde2a"},{"o":1,"v":"80%","p":80,"i":"42e92759"}],"r":[{"o":0,"a":"Custom1","t":6,"c":" 1.0.0 ","v":"< 1.0.0","i":"0c27d053"}]},"relations":{"v":"Default","t":1,"i":"c6155773","p":[],"r":[{"o":0,"a":"Custom1","t":6,"c":"1.0.0,","v":"<1.0.0,","i":"21b31b61"},{"o":1,"a":"Custom1","t":6,"c":"1.0.0","v":"< 1.0.0","i":"db3ddb7d"},{"o":2,"a":"Custom1","t":7,"c":"1.0.0","v":"<=1.0.0","i":"aa2c7493"},{"o":3,"a":"Custom1","t":8,"c":"2.0.0","v":">2.0.0","i":"5e47a1ea"},{"o":4,"a":"Custom1","t":9,"c":"2.0.0","v":">=2.0.0","i":"99482756"}]}}}""" + """{ + "p":{ + "u":"https://cdn-global.configcat.com", + "r":0, + "s":"ckQuggJzKXlXnTu55hP6Ttabx2elF0NW2rQYsBX0leg=" + }, + "f":{ + "isNotOneOf":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":5, + "l":[ + "1.0.0", + "1.0.1", + "2.0.0", + "2.0.1", + "2.0.2" + ] + } + } + ], + "s":{ + "v":{ + "s":"Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, )" + }, + "i":"a8d5f278" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":5, + "l":[ + "1.0.0", + "3.0.1" + ] + } + } + ], + "s":{ + "v":{ + "s":"Is not one of (1.0.0, 3.0.1)" + }, + "i":"54ac757f" + } + } + ], + "v":{ + "s":"Default" + }, + "i":"f79b763d" + }, + "isNotOneOfWithPercentage":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":5, + "l":[ + "1.0.0", + "1.0.1", + "2.0.0", + "2.0.1", + "2.0.2" + ] + } + } + ], + "s":{ + "v":{ + "s":"Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, )" + }, + "i":"9bf9e66f" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":5, + "l":[ + "1.0.0", + "3.0.1" + ] + } + } + ], + "s":{ + "v":{ + "s":"Is not one of (1.0.0, 3.0.1)" + }, + "i":"bfc1a544" + } + } + ], + "p":[ + { + "p":20, + "v":{ + "s":"20%" + }, + "i":"68f652f0" + }, + { + "p":80, + "v":{ + "s":"80%" + }, + "i":"b8d926e0" + } + ], + "v":{ + "s":"Default" + }, + "i":"b9614bad" + }, + "isOneOf":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":4, + "l":[ + "1.0.0", + "2" + ] + } + } + ], + "s":{ + "v":{ + "s":"Is one of (1.0.0, 2)" + }, + "i":"1e934047" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":4, + "l":[ + "1.0.0" + ] + } + } + ], + "s":{ + "v":{ + "s":"Is one of (1.0.0)" + }, + "i":"44342254" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":4, + "l":[ + "2.0.1", + "2.0.2" + ] + } + } + ], + "s":{ + "v":{ + "s":"Is one of ( , 2.0.1, 2.0.2, )" + }, + "i":"90e3ef46" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":4, + "l":[ + "3......" + ] + } + } + ], + "s":{ + "v":{ + "s":"Is one of (3......)" + }, + "i":"59523971" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":4, + "l":[ + "3...." + ] + } + } + ], + "s":{ + "v":{ + "s":"Is one of (3...)" + }, + "i":"2de217a1" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":4, + "l":[ + "3..0" + ] + } + } + ], + "s":{ + "v":{ + "s":"Is one of (3..0)" + }, + "i":"bf943c79" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":4, + "l":[ + "3.0" + ] + } + } + ], + "s":{ + "v":{ + "s":"Is one of (3.0)" + }, + "i":"3a6a8077" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":4, + "l":[ + "3.0." + ] + } + } + ], + "s":{ + "v":{ + "s":"Is one of (3.0.)" + }, + "i":"44f25fed" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":4, + "l":[ + "3.0.0" + ] + } + } + ], + "s":{ + "v":{ + "s":"Is one of (3.0.0)" + }, + "i":"e77f5306" + } + } + ], + "v":{ + "s":"Default" + }, + "i":"c4ec4d53" + }, + "isOneOfWithPercentage":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":4, + "l":[ + "1.0.0" + ] + } + } + ], + "s":{ + "v":{ + "s":"is one of (1.0.0)" + }, + "i":"0ac4afc1" + } + } + ], + "p":[ + { + "p":20, + "v":{ + "s":"20%" + }, + "i":"e25dba31" + }, + { + "p":80, + "v":{ + "s":"80%" + }, + "i":"8c70c181" + } + ], + "v":{ + "s":"Default" + }, + "i":"a94ff896" + }, + "lessThanWithPercentage":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":6, + "s":"1.0.0" + } + } + ], + "s":{ + "v":{ + "s":"\u003C 1.0.0" + }, + "i":"0c27d053" + } + } + ], + "p":[ + { + "p":20, + "v":{ + "s":"20%" + }, + "i":"3b1fde2a" + }, + { + "p":80, + "v":{ + "s":"80%" + }, + "i":"42e92759" + } + ], + "v":{ + "s":"Default" + }, + "i":"0081c525" + }, + "relations":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":6, + "s":"1.0.0," + } + } + ], + "s":{ + "v":{ + "s":"\u003C1.0.0," + }, + "i":"21b31b61" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":6, + "s":"1.0.0" + } + } + ], + "s":{ + "v":{ + "s":"\u003C 1.0.0" + }, + "i":"db3ddb7d" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":7, + "s":"1.0.0" + } + } + ], + "s":{ + "v":{ + "s":"\u003C=1.0.0" + }, + "i":"aa2c7493" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":8, + "s":"2.0.0" + } + } + ], + "s":{ + "v":{ + "s":"\u003E2.0.0" + }, + "i":"5e47a1ea" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":9, + "s":"2.0.0" + } + } + ], + "s":{ + "v":{ + "s":"\u003E=2.0.0" + }, + "i":"99482756" + } + } + ], + "v":{ + "s":"Default" + }, + "i":"c6155773" + } + } +}""" } diff --git a/src/commonTest/kotlin/com/configcat/integration/matrix/SemanticMatrix2.kt b/src/commonTest/kotlin/com/configcat/integration/matrix/SemanticMatrix2.kt index 5cbabb82..1cbbba58 100644 --- a/src/commonTest/kotlin/com/configcat/integration/matrix/SemanticMatrix2.kt +++ b/src/commonTest/kotlin/com/configcat/integration/matrix/SemanticMatrix2.kt @@ -1,7 +1,7 @@ package com.configcat.integration.matrix object SemanticMatrix2 : DataMatrix { - override val sdkKey = "PKDVCLf-Hq-h-kCzMp-L7Q/q6jMCFIp-EmuAfnmZhPY7w" + override val sdkKey: String = "configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/U8nt3zEhDEO5S2ulubCopA" override val data = """Identifier;Email;Country;AppVersion;precedenceTests dontcare;;;1.9.1-1;< 1.9.1-2 dontcare;;;1.9.1-2;< 1.9.1-10 @@ -98,5 +98,1417 @@ dontcare;;;50.60.71-patch1+anothermetadata;>= 50.60.71-patch1+metadata dontcare;;;40.0.0-patch;>= 40.0.0-patch dontcare;;;30.0.0-beta;>= 30.0.0-alpha""" override val remoteJson = - """{"p":{"u":"https://cdn-global.configcat.com","r":0},"f":{"precedenceTests":{"v":"DEFAULT-FROM-CC-APP","t":1,"i":"53940653","p":[],"r":[{"o":0,"a":"AppVersion","t":6,"c":"1.9.1-2","v":"< 1.9.1-2","i":"92a04969"},{"o":1,"a":"AppVersion","t":6,"c":"1.9.1-10","v":"< 1.9.1-10","i":"c651eba2"},{"o":2,"a":"AppVersion","t":6,"c":"1.9.1-10a","v":"< 1.9.1-10a","i":"237dedc5"},{"o":3,"a":"AppVersion","t":6,"c":"1.9.1-1a","v":"< 1.9.1-1a","i":"154a319b"},{"o":4,"a":"AppVersion","t":6,"c":"1.9.1-alpha","v":"< 1.9.1-alpha","i":"33f59c5e"},{"o":5,"a":"AppVersion","t":6,"c":"1.9.99-alpha","v":"< 1.9.99-alpha","i":"9b6c24f1"},{"o":6,"a":"AppVersion","t":4,"c":"1.9.99-alpha","v":"= 1.9.99-alpha","i":"c08a99de"},{"o":7,"a":"AppVersion","t":6,"c":"1.9.99-beta","v":"< 1.9.99-beta","i":"4c9d7eb1"},{"o":8,"a":"AppVersion","t":6,"c":"1.9.99-rc","v":"< 1.9.99-rc","i":"e5aa7655"},{"o":9,"a":"AppVersion","t":6,"c":"1.9.99-rc.1","v":"< 1.9.99-rc.1","i":"c9075e5b"},{"o":10,"a":"AppVersion","t":6,"c":"1.9.99-rc.2","v":"< 1.9.99-rc.2","i":"97465d24"},{"o":11,"a":"AppVersion","t":6,"c":"1.9.99-rc.20","v":"< 1.9.99-rc.20","i":"32d20254"},{"o":12,"a":"AppVersion","t":6,"c":"1.9.99-rc.20a","v":"< 1.9.99-rc.20a","i":"c4843bfb"},{"o":13,"a":"AppVersion","t":6,"c":"1.9.99-rc.2a","v":"< 1.9.99-rc.2a","i":"11b96c5a"},{"o":14,"a":"AppVersion","t":6,"c":"1.9.99","v":"< 1.9.99","i":"dc5a0ed1"},{"o":15,"a":"AppVersion","t":6,"c":"1.9.100","v":"< 1.9.100","i":"8ce0bff8"},{"o":16,"a":"AppVersion","t":6,"c":"1.10.0-alpha","v":"< 1.10.0-alpha","i":"9ff0cadc"},{"o":17,"a":"AppVersion","t":7,"c":"1.10.0-alpha","v":"<= 1.10.0-alpha","i":"7a24a0f6"},{"o":18,"a":"AppVersion","t":6,"c":"1.10.0","v":"< 1.10.0","i":"03a85e10"},{"o":19,"a":"AppVersion","t":7,"c":"1.10.0","v":"<= 1.10.0","i":"b37d5427"},{"o":20,"a":"AppVersion","t":7,"c":"1.10.1","v":"<= 1.10.1","i":"b402f112"},{"o":21,"a":"AppVersion","t":7,"c":"1.10.3","v":"<= 1.10.3","i":"da563c51"},{"o":22,"a":"AppVersion","t":6,"c":"2.0.0","v":"< 2.0.0","i":"c64645a1"},{"o":23,"a":"AppVersion","t":4,"c":"2.0.0","v":"= 2.0.0","i":"b0008e97"},{"o":24,"a":"AppVersion","t":4,"c":"3.0.0+build3","v":"= 3.0.0+build3","i":"67ceff4e"},{"o":25,"a":"AppVersion","t":4,"c":"4.0.0+001","v":"= 4.0.0+001","i":"da6dd7ab"},{"o":26,"a":"AppVersion","t":4,"c":"5.0.0+20130313144700","v":"= 5.0.0+20130313144700","i":"673b3fd5"},{"o":27,"a":"AppVersion","t":4,"c":"6.0.0+exp.sha.5114f85","v":"= 6.0.0+exp.sha.5114f85","i":"e3bcafe6"},{"o":28,"a":"AppVersion","t":4,"c":"7.0.0-patch","v":"= 7.0.0-patch","i":"04e2949b"},{"o":29,"a":"AppVersion","t":4,"c":"8.0.0-patch+anothermetadata","v":"= 8.0.0-patch+anothermetadata","i":"505e8efa"},{"o":30,"a":"AppVersion","t":4,"c":"9.0.0-patch+metadata","v":"= 9.0.0-patch+metadata","i":"ca4c9dcc"},{"o":31,"a":"AppVersion","t":8,"c":"103.0.0","v":"> 103.0.0","i":"9428e733"},{"o":32,"a":"AppVersion","t":9,"c":"103.0.0","v":">= 103.0.0","i":"c448abb8"},{"o":33,"a":"AppVersion","t":9,"c":"101.0.0","v":">= 101.0.0","i":"9980c03a"},{"o":34,"a":"AppVersion","t":8,"c":"90.103.0","v":"> 90.103.0","i":"04259f0b"},{"o":35,"a":"AppVersion","t":9,"c":"90.103.0","v":">= 90.103.0","i":"4817782c"},{"o":36,"a":"AppVersion","t":9,"c":"90.101.0","v":">= 90.101.0","i":"2e9be278"},{"o":37,"a":"AppVersion","t":8,"c":"80.0.103","v":"> 80.0.103","i":"d7058d3e"},{"o":38,"a":"AppVersion","t":9,"c":"80.0.103","v":">= 80.0.103","i":"0da87e6b"},{"o":39,"a":"AppVersion","t":9,"c":"80.0.101","v":">= 80.0.101","i":"8e71aa24"},{"o":40,"a":"AppVersion","t":9,"c":"73.0.0-beta.2","v":">= 73.0.0-beta.2","i":"26a443e3"},{"o":41,"a":"AppVersion","t":8,"c":"72.0.0-beta.2","v":"> 72.0.0-beta.2","i":"0705710a"},{"o":42,"a":"AppVersion","t":8,"c":"72.0.0-beta.1","v":"> 72.0.0-beta.1","i":"7d6cf793"},{"o":43,"a":"AppVersion","t":8,"c":"72.0.0-beta","v":"> 72.0.0-beta","i":"f9ef6e83"},{"o":44,"a":"AppVersion","t":8,"c":"72.0.0-alpha","v":"> 72.0.0-alpha","i":"cf17c939"},{"o":45,"a":"AppVersion","t":8,"c":"72.0.0-1a","v":"> 72.0.0-1a","i":"650640fd"},{"o":46,"a":"AppVersion","t":8,"c":"72.0.0-10a","v":"> 72.0.0-10a","i":"508dd0b2"},{"o":47,"a":"AppVersion","t":8,"c":"72.0.0-2","v":"> 72.0.0-2","i":"142e6d61"},{"o":48,"a":"AppVersion","t":8,"c":"72.0.0-1","v":"> 72.0.0-1","i":"d969006a"},{"o":49,"a":"AppVersion","t":9,"c":"71.0.0+anothermetadata","v":">= 71.0.0+anothermetadata","i":"6f74dc87"},{"o":50,"a":"AppVersion","t":9,"c":"71.0.0-patch3+anothermetadata","v":">= 71.0.0-patch3+anothermetadata","i":"8061734b"},{"o":51,"a":"AppVersion","t":9,"c":"71.0.0-patch2","v":">= 71.0.0-patch2","i":"0615c726"},{"o":52,"a":"AppVersion","t":9,"c":"71.0.0-patch1+metadata","v":">= 71.0.0-patch1+metadata","i":"910b79b5"},{"o":53,"a":"AppVersion","t":9,"c":"60.73.0-beta.2","v":">= 60.73.0-beta.2","i":"32e2a4ea"},{"o":54,"a":"AppVersion","t":8,"c":"60.72.0-beta.2","v":"> 60.72.0-beta.2","i":"9017539e"},{"o":55,"a":"AppVersion","t":8,"c":"60.72.0-beta.1","v":"> 60.72.0-beta.1","i":"74de4704"},{"o":56,"a":"AppVersion","t":8,"c":"60.72.0-beta","v":"> 60.72.0-beta","i":"b61af046"},{"o":57,"a":"AppVersion","t":8,"c":"60.72.0-alpha","v":"> 60.72.0-alpha","i":"419eb18d"},{"o":58,"a":"AppVersion","t":8,"c":"60.72.0-1a","v":"> 60.72.0-1a","i":"7574c707"},{"o":59,"a":"AppVersion","t":8,"c":"60.72.0-10a","v":"> 60.72.0-10a","i":"5b3949e6"},{"o":60,"a":"AppVersion","t":8,"c":"60.72.0-2","v":"> 60.72.0-2","i":"9ff17692"},{"o":61,"a":"AppVersion","t":8,"c":"60.72.0-1","v":"> 60.72.0-1","i":"3027451d"},{"o":62,"a":"AppVersion","t":9,"c":"60.71.0+anothermetadata","v":">= 60.71.0+anothermetadata","i":"613d3642"},{"o":63,"a":"AppVersion","t":9,"c":"60.71.0-patch3+anothermetadata","v":">= 60.71.0-patch3+anothermetadata","i":"e45ffb06"},{"o":64,"a":"AppVersion","t":9,"c":"60.71.0-patch2","v":">= 60.71.0-patch2","i":"db50de0a"},{"o":65,"a":"AppVersion","t":9,"c":"60.71.0-patch1+metadata","v":">= 60.71.0-patch1+metadata","i":"5f9acaf7"},{"o":66,"a":"AppVersion","t":9,"c":"50.60.73-beta.2","v":">= 50.60.73-beta.2","i":"701ac6b2"},{"o":67,"a":"AppVersion","t":8,"c":"50.60.72-beta.2","v":"> 50.60.72-beta.2","i":"da09daf8"},{"o":68,"a":"AppVersion","t":8,"c":"50.60.72-beta.1","v":"> 50.60.72-beta.1","i":"8f7e54d5"},{"o":69,"a":"AppVersion","t":8,"c":"50.60.72-beta","v":"> 50.60.72-beta","i":"93e245a5"},{"o":70,"a":"AppVersion","t":8,"c":"50.60.72-alpha","v":"> 50.60.72-alpha","i":"356c8279"},{"o":71,"a":"AppVersion","t":8,"c":"50.60.72-1a","v":"> 50.60.72-1a","i":"6131df16"},{"o":72,"a":"AppVersion","t":8,"c":"50.60.72-10a","v":"> 50.60.72-10a","i":"3f1a3aa4"},{"o":73,"a":"AppVersion","t":8,"c":"50.60.72-2","v":"> 50.60.72-2","i":"7534cc57"},{"o":74,"a":"AppVersion","t":8,"c":"50.60.72-1","v":"> 50.60.72-1","i":"24d6f0ab"},{"o":75,"a":"AppVersion","t":9,"c":"50.60.71+anothermetadata","v":">= 50.60.71+anothermetadata","i":"fdd36d82"},{"o":76,"a":"AppVersion","t":9,"c":"50.60.71-patch3+anothermetadata","v":">= 50.60.71-patch3+anothermetadata","i":"709780e6"},{"o":77,"a":"AppVersion","t":9,"c":"50.60.71-patch2","v":">= 50.60.71-patch2","i":"7649322d"},{"o":78,"a":"AppVersion","t":9,"c":"50.60.71-patch1+metadata","v":">= 50.60.71-patch1+metadata","i":"25d5c70d"},{"o":79,"a":"AppVersion","t":9,"c":"40.0.0-patch","v":">= 40.0.0-patch","i":"271370ff"},{"o":80,"a":"AppVersion","t":9,"c":"30.0.0-alpha","v":">= 30.0.0-alpha","i":"af29c39d"}]}}}""" + """{ + "p":{ + "u":"https://cdn-global.configcat.com", + "r":0, + "s":"2V43LyfkDnQoRg0q5qOcqKwg4y0GT/anDuIpG2yFEUA=" + }, + "f":{ + "precedenceTests":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":6, + "s":"1.9.1-2" + } + } + ], + "s":{ + "v":{ + "s":"\u003C 1.9.1-2" + }, + "i":"92a04969" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":6, + "s":"1.9.1-10" + } + } + ], + "s":{ + "v":{ + "s":"\u003C 1.9.1-10" + }, + "i":"c651eba2" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":6, + "s":"1.9.1-10a" + } + } + ], + "s":{ + "v":{ + "s":"\u003C 1.9.1-10a" + }, + "i":"237dedc5" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":6, + "s":"1.9.1-1a" + } + } + ], + "s":{ + "v":{ + "s":"\u003C 1.9.1-1a" + }, + "i":"154a319b" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":6, + "s":"1.9.1-alpha" + } + } + ], + "s":{ + "v":{ + "s":"\u003C 1.9.1-alpha" + }, + "i":"33f59c5e" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":6, + "s":"1.9.99-alpha" + } + } + ], + "s":{ + "v":{ + "s":"\u003C 1.9.99-alpha" + }, + "i":"9b6c24f1" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":4, + "l":[ + "1.9.99-alpha" + ] + } + } + ], + "s":{ + "v":{ + "s":"= 1.9.99-alpha" + }, + "i":"c08a99de" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":6, + "s":"1.9.99-beta" + } + } + ], + "s":{ + "v":{ + "s":"\u003C 1.9.99-beta" + }, + "i":"4c9d7eb1" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":6, + "s":"1.9.99-rc" + } + } + ], + "s":{ + "v":{ + "s":"\u003C 1.9.99-rc" + }, + "i":"e5aa7655" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":6, + "s":"1.9.99-rc.1" + } + } + ], + "s":{ + "v":{ + "s":"\u003C 1.9.99-rc.1" + }, + "i":"c9075e5b" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":6, + "s":"1.9.99-rc.2" + } + } + ], + "s":{ + "v":{ + "s":"\u003C 1.9.99-rc.2" + }, + "i":"97465d24" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":6, + "s":"1.9.99-rc.20" + } + } + ], + "s":{ + "v":{ + "s":"\u003C 1.9.99-rc.20" + }, + "i":"32d20254" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":6, + "s":"1.9.99-rc.20a" + } + } + ], + "s":{ + "v":{ + "s":"\u003C 1.9.99-rc.20a" + }, + "i":"c4843bfb" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":6, + "s":"1.9.99-rc.2a" + } + } + ], + "s":{ + "v":{ + "s":"\u003C 1.9.99-rc.2a" + }, + "i":"11b96c5a" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":6, + "s":"1.9.99" + } + } + ], + "s":{ + "v":{ + "s":"\u003C 1.9.99" + }, + "i":"dc5a0ed1" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":6, + "s":"1.9.100" + } + } + ], + "s":{ + "v":{ + "s":"\u003C 1.9.100" + }, + "i":"8ce0bff8" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":6, + "s":"1.10.0-alpha" + } + } + ], + "s":{ + "v":{ + "s":"\u003C 1.10.0-alpha" + }, + "i":"9ff0cadc" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":7, + "s":"1.10.0-alpha" + } + } + ], + "s":{ + "v":{ + "s":"\u003C= 1.10.0-alpha" + }, + "i":"7a24a0f6" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":6, + "s":"1.10.0" + } + } + ], + "s":{ + "v":{ + "s":"\u003C 1.10.0" + }, + "i":"03a85e10" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":7, + "s":"1.10.0" + } + } + ], + "s":{ + "v":{ + "s":"\u003C= 1.10.0" + }, + "i":"b37d5427" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":7, + "s":"1.10.1" + } + } + ], + "s":{ + "v":{ + "s":"\u003C= 1.10.1" + }, + "i":"b402f112" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":7, + "s":"1.10.3" + } + } + ], + "s":{ + "v":{ + "s":"\u003C= 1.10.3" + }, + "i":"da563c51" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":6, + "s":"2.0.0" + } + } + ], + "s":{ + "v":{ + "s":"\u003C 2.0.0" + }, + "i":"c64645a1" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":4, + "l":[ + "2.0.0" + ] + } + } + ], + "s":{ + "v":{ + "s":"= 2.0.0" + }, + "i":"b0008e97" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":4, + "l":[ + "3.0.0\u002Bbuild3" + ] + } + } + ], + "s":{ + "v":{ + "s":"= 3.0.0\u002Bbuild3" + }, + "i":"67ceff4e" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":4, + "l":[ + "4.0.0\u002B001" + ] + } + } + ], + "s":{ + "v":{ + "s":"= 4.0.0\u002B001" + }, + "i":"da6dd7ab" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":4, + "l":[ + "5.0.0\u002B20130313144700" + ] + } + } + ], + "s":{ + "v":{ + "s":"= 5.0.0\u002B20130313144700" + }, + "i":"673b3fd5" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":4, + "l":[ + "6.0.0\u002Bexp.sha.5114f85" + ] + } + } + ], + "s":{ + "v":{ + "s":"= 6.0.0\u002Bexp.sha.5114f85" + }, + "i":"e3bcafe6" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":4, + "l":[ + "7.0.0-patch" + ] + } + } + ], + "s":{ + "v":{ + "s":"= 7.0.0-patch" + }, + "i":"04e2949b" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":4, + "l":[ + "8.0.0-patch\u002Banothermetadata" + ] + } + } + ], + "s":{ + "v":{ + "s":"= 8.0.0-patch\u002Banothermetadata" + }, + "i":"505e8efa" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":4, + "l":[ + "9.0.0-patch\u002Bmetadata" + ] + } + } + ], + "s":{ + "v":{ + "s":"= 9.0.0-patch\u002Bmetadata" + }, + "i":"ca4c9dcc" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":8, + "s":"103.0.0" + } + } + ], + "s":{ + "v":{ + "s":"\u003E 103.0.0" + }, + "i":"9428e733" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":9, + "s":"103.0.0" + } + } + ], + "s":{ + "v":{ + "s":"\u003E= 103.0.0" + }, + "i":"c448abb8" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":9, + "s":"101.0.0" + } + } + ], + "s":{ + "v":{ + "s":"\u003E= 101.0.0" + }, + "i":"9980c03a" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":8, + "s":"90.103.0" + } + } + ], + "s":{ + "v":{ + "s":"\u003E 90.103.0" + }, + "i":"04259f0b" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":9, + "s":"90.103.0" + } + } + ], + "s":{ + "v":{ + "s":"\u003E= 90.103.0" + }, + "i":"4817782c" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":9, + "s":"90.101.0" + } + } + ], + "s":{ + "v":{ + "s":"\u003E= 90.101.0" + }, + "i":"2e9be278" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":8, + "s":"80.0.103" + } + } + ], + "s":{ + "v":{ + "s":"\u003E 80.0.103" + }, + "i":"d7058d3e" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":9, + "s":"80.0.103" + } + } + ], + "s":{ + "v":{ + "s":"\u003E= 80.0.103" + }, + "i":"0da87e6b" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":9, + "s":"80.0.101" + } + } + ], + "s":{ + "v":{ + "s":"\u003E= 80.0.101" + }, + "i":"8e71aa24" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":9, + "s":"73.0.0-beta.2" + } + } + ], + "s":{ + "v":{ + "s":"\u003E= 73.0.0-beta.2" + }, + "i":"26a443e3" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":8, + "s":"72.0.0-beta.2" + } + } + ], + "s":{ + "v":{ + "s":"\u003E 72.0.0-beta.2" + }, + "i":"0705710a" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":8, + "s":"72.0.0-beta.1" + } + } + ], + "s":{ + "v":{ + "s":"\u003E 72.0.0-beta.1" + }, + "i":"7d6cf793" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":8, + "s":"72.0.0-beta" + } + } + ], + "s":{ + "v":{ + "s":"\u003E 72.0.0-beta" + }, + "i":"f9ef6e83" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":8, + "s":"72.0.0-alpha" + } + } + ], + "s":{ + "v":{ + "s":"\u003E 72.0.0-alpha" + }, + "i":"cf17c939" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":8, + "s":"72.0.0-1a" + } + } + ], + "s":{ + "v":{ + "s":"\u003E 72.0.0-1a" + }, + "i":"650640fd" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":8, + "s":"72.0.0-10a" + } + } + ], + "s":{ + "v":{ + "s":"\u003E 72.0.0-10a" + }, + "i":"508dd0b2" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":8, + "s":"72.0.0-2" + } + } + ], + "s":{ + "v":{ + "s":"\u003E 72.0.0-2" + }, + "i":"142e6d61" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":8, + "s":"72.0.0-1" + } + } + ], + "s":{ + "v":{ + "s":"\u003E 72.0.0-1" + }, + "i":"d969006a" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":9, + "s":"71.0.0\u002Banothermetadata" + } + } + ], + "s":{ + "v":{ + "s":"\u003E= 71.0.0\u002Banothermetadata" + }, + "i":"6f74dc87" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":9, + "s":"71.0.0-patch3\u002Banothermetadata" + } + } + ], + "s":{ + "v":{ + "s":"\u003E= 71.0.0-patch3\u002Banothermetadata" + }, + "i":"8061734b" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":9, + "s":"71.0.0-patch2" + } + } + ], + "s":{ + "v":{ + "s":"\u003E= 71.0.0-patch2" + }, + "i":"0615c726" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":9, + "s":"71.0.0-patch1\u002Bmetadata" + } + } + ], + "s":{ + "v":{ + "s":"\u003E= 71.0.0-patch1\u002Bmetadata" + }, + "i":"910b79b5" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":9, + "s":"60.73.0-beta.2" + } + } + ], + "s":{ + "v":{ + "s":"\u003E= 60.73.0-beta.2" + }, + "i":"32e2a4ea" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":8, + "s":"60.72.0-beta.2" + } + } + ], + "s":{ + "v":{ + "s":"\u003E 60.72.0-beta.2" + }, + "i":"9017539e" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":8, + "s":"60.72.0-beta.1" + } + } + ], + "s":{ + "v":{ + "s":"\u003E 60.72.0-beta.1" + }, + "i":"74de4704" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":8, + "s":"60.72.0-beta" + } + } + ], + "s":{ + "v":{ + "s":"\u003E 60.72.0-beta" + }, + "i":"b61af046" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":8, + "s":"60.72.0-alpha" + } + } + ], + "s":{ + "v":{ + "s":"\u003E 60.72.0-alpha" + }, + "i":"419eb18d" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":8, + "s":"60.72.0-1a" + } + } + ], + "s":{ + "v":{ + "s":"\u003E 60.72.0-1a" + }, + "i":"7574c707" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":8, + "s":"60.72.0-10a" + } + } + ], + "s":{ + "v":{ + "s":"\u003E 60.72.0-10a" + }, + "i":"5b3949e6" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":8, + "s":"60.72.0-2" + } + } + ], + "s":{ + "v":{ + "s":"\u003E 60.72.0-2" + }, + "i":"9ff17692" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":8, + "s":"60.72.0-1" + } + } + ], + "s":{ + "v":{ + "s":"\u003E 60.72.0-1" + }, + "i":"3027451d" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":9, + "s":"60.71.0\u002Banothermetadata" + } + } + ], + "s":{ + "v":{ + "s":"\u003E= 60.71.0\u002Banothermetadata" + }, + "i":"613d3642" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":9, + "s":"60.71.0-patch3\u002Banothermetadata" + } + } + ], + "s":{ + "v":{ + "s":"\u003E= 60.71.0-patch3\u002Banothermetadata" + }, + "i":"e45ffb06" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":9, + "s":"60.71.0-patch2" + } + } + ], + "s":{ + "v":{ + "s":"\u003E= 60.71.0-patch2" + }, + "i":"db50de0a" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":9, + "s":"60.71.0-patch1\u002Bmetadata" + } + } + ], + "s":{ + "v":{ + "s":"\u003E= 60.71.0-patch1\u002Bmetadata" + }, + "i":"5f9acaf7" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":9, + "s":"50.60.73-beta.2" + } + } + ], + "s":{ + "v":{ + "s":"\u003E= 50.60.73-beta.2" + }, + "i":"701ac6b2" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":8, + "s":"50.60.72-beta.2" + } + } + ], + "s":{ + "v":{ + "s":"\u003E 50.60.72-beta.2" + }, + "i":"da09daf8" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":8, + "s":"50.60.72-beta.1" + } + } + ], + "s":{ + "v":{ + "s":"\u003E 50.60.72-beta.1" + }, + "i":"8f7e54d5" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":8, + "s":"50.60.72-beta" + } + } + ], + "s":{ + "v":{ + "s":"\u003E 50.60.72-beta" + }, + "i":"93e245a5" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":8, + "s":"50.60.72-alpha" + } + } + ], + "s":{ + "v":{ + "s":"\u003E 50.60.72-alpha" + }, + "i":"356c8279" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":8, + "s":"50.60.72-1a" + } + } + ], + "s":{ + "v":{ + "s":"\u003E 50.60.72-1a" + }, + "i":"6131df16" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":8, + "s":"50.60.72-10a" + } + } + ], + "s":{ + "v":{ + "s":"\u003E 50.60.72-10a" + }, + "i":"3f1a3aa4" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":8, + "s":"50.60.72-2" + } + } + ], + "s":{ + "v":{ + "s":"\u003E 50.60.72-2" + }, + "i":"7534cc57" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":8, + "s":"50.60.72-1" + } + } + ], + "s":{ + "v":{ + "s":"\u003E 50.60.72-1" + }, + "i":"24d6f0ab" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":9, + "s":"50.60.71\u002Banothermetadata" + } + } + ], + "s":{ + "v":{ + "s":"\u003E= 50.60.71\u002Banothermetadata" + }, + "i":"fdd36d82" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":9, + "s":"50.60.71-patch3\u002Banothermetadata" + } + } + ], + "s":{ + "v":{ + "s":"\u003E= 50.60.71-patch3\u002Banothermetadata" + }, + "i":"709780e6" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":9, + "s":"50.60.71-patch2" + } + } + ], + "s":{ + "v":{ + "s":"\u003E= 50.60.71-patch2" + }, + "i":"7649322d" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":9, + "s":"50.60.71-patch1\u002Bmetadata" + } + } + ], + "s":{ + "v":{ + "s":"\u003E= 50.60.71-patch1\u002Bmetadata" + }, + "i":"25d5c70d" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":9, + "s":"40.0.0-patch" + } + } + ], + "s":{ + "v":{ + "s":"\u003E= 40.0.0-patch" + }, + "i":"271370ff" + } + }, + { + "c":[ + { + "u":{ + "a":"AppVersion", + "c":9, + "s":"30.0.0-alpha" + } + } + ], + "s":{ + "v":{ + "s":"\u003E= 30.0.0-alpha" + }, + "i":"af29c39d" + } + } + ], + "v":{ + "s":"DEFAULT-FROM-CC-APP" + }, + "i":"53940653" + } + } +}""" } diff --git a/src/commonTest/kotlin/com/configcat/integration/matrix/SensitiveMatrix.kt b/src/commonTest/kotlin/com/configcat/integration/matrix/SensitiveMatrix.kt index e7b1363c..6f7644d6 100644 --- a/src/commonTest/kotlin/com/configcat/integration/matrix/SensitiveMatrix.kt +++ b/src/commonTest/kotlin/com/configcat/integration/matrix/SensitiveMatrix.kt @@ -1,7 +1,7 @@ package com.configcat.integration.matrix object SensitiveMatrix : DataMatrix { - override val sdkKey = "PKDVCLf-Hq-h-kCzMp-L7Q/qX3TP2dTj06ZpCCT1h_SPA" + override val sdkKey: String = "configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/-0YmVOUNgEGKkgRF-rU65g" override val data = """Identifier;Email;Country;Custom1;isOneOfSensitive;isNotOneOfSensitive ##null##;;;;ToAll;ToAll id1;macska@example.com;;;Macska;Kigyo @@ -11,5 +11,152 @@ Kutya;macska@example.com;;;Macska;ToAll id1;;Scotland;;Britt;Kigyo Macska;;USA;;ToAll;Ireland""" override val remoteJson = - """{"p":{"u":"https://cdn-global.configcat.com","r":0},"f":{"isNotOneOfSensitive":{"v":"ToAll","t":1,"i":"97bd663d","p":[],"r":[{"o":0,"a":"Identifier","t":17,"c":"68d93aa74a0aa1664f65ad6c0515f24769b15c84,8409e4e5d27a1465165012b03b2606f0e5b08250","v":"Kigyo","i":"4e4356b4"},{"o":1,"a":"Email","t":17,"c":"2e1c7263a639cf2719f585dfa0be3953c13dd36f,532df0aa59af3cf1d3d876316225e987e63bf8a6","v":"Angolna","i":"d75ea4a4"},{"o":2,"a":"Country","t":17,"c":"707fe00aa123eb0be5010f1d3065c2b6d7934ca4,ff95dc990b9440c8ff18edd8592bf43915e510b9,e2ff49d5209adefb1d572ca4ca42701ac5b167ad","v":"Ireland","i":"e8826a82"}]},"isOneOfSensitive":{"v":"ToAll","t":1,"i":"71a78b2a","p":[],"r":[{"o":0,"a":"Email","t":16,"c":"532df0aa59af3cf1d3d876316225e987e63bf8a6","v":"Macska","i":"b1dc4d99"},{"o":1,"a":"Identifier","t":16,"c":"cc1a672b80f85ec48aa620a588864285e2b04a45,68d93aa74a0aa1664f65ad6c0515f24769b15c84","v":"Allat","i":"fb7be8fd"},{"o":2,"a":"Country","t":16,"c":"707fe00aa123eb0be5010f1d3065c2b6d7934ca4,ff95dc990b9440c8ff18edd8592bf43915e510b9,e2ff49d5209adefb1d572ca4ca42701ac5b167ad","v":"Britt","i":"f1b9ed25"}]}}}""" + """{ + "p":{ + "u":"https://cdn-global.configcat.com", + "r":0, + "s":"pgy7CictnJJD59UhungIc\u002BGSmms7YMn2krC9qrLpSiQ=" + }, + "f":{ + "isNotOneOfSensitive":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Identifier", + "c":17, + "l":[ + "ca3331a482e27456ea7d7bd0945e3ae4234b75364633472c228015e793d0f816", + "dafe3c25703e720bdb610a3a8f09e7ee4d551b076344cadf9d2128c57e06fa6b" + ] + } + } + ], + "s":{ + "v":{ + "s":"Kigyo" + }, + "i":"4e4356b4" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":17, + "l":[ + "7a3045eee9d4622a44a3e325eae48efefd75a3a308a523d7b52396b9598ab89a", + "f798e73a624d528559710c88c69e0cb95a08fc0c7d05b856fc92d19213ac7f78" + ] + } + } + ], + "s":{ + "v":{ + "s":"Angolna" + }, + "i":"d75ea4a4" + } + }, + { + "c":[ + { + "u":{ + "a":"Country", + "c":17, + "l":[ + "00a23f647669f35d157a7fd6c0547ea856d90c44175316dd131e9f6fda1a2e77", + "c62b92619bab9ef8de62fc5ee1ce061f026646098bb92ef184970c88a3b17179", + "d6d3219369c112509674c1ba8905802a40b8d4b40298dfc617f3b4f86634b391" + ] + } + } + ], + "s":{ + "v":{ + "s":"Ireland" + }, + "i":"e8826a82" + } + } + ], + "v":{ + "s":"ToAll" + }, + "i":"97bd663d" + }, + "isOneOfSensitive":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":16, + "l":[ + "8592127ea4120babddd53ba4462b896a3a9e451f64896aa890a9a7ce2d271515" + ] + } + } + ], + "s":{ + "v":{ + "s":"Macska" + }, + "i":"b1dc4d99" + } + }, + { + "c":[ + { + "u":{ + "a":"Identifier", + "c":16, + "l":[ + "2b3e892bd9af98af5c91d8ab403c0babc4745b7e0668f331a6a16c7087796fdb", + "6667c8a52e00103fff2b701e635a6c6b3c78b31123e46aa275ca1ee36b7b3544" + ] + } + } + ], + "s":{ + "v":{ + "s":"Allat" + }, + "i":"fb7be8fd" + } + }, + { + "c":[ + { + "u":{ + "a":"Country", + "c":16, + "l":[ + "892e9b4091539ee5485d3ee05b7d0d51360258dee4b264833ec84f0a1abab074", + "493f32ed8aec79c0faf2dccfd76ab54596a746694b35de707b3b5e6498bf97a7", + "808b3f5fae63871c12ca51f325c44c63c33f394ceb1fc3f6e877b907bcf19ff9" + ] + } + } + ], + "s":{ + "v":{ + "s":"Britt" + }, + "i":"f1b9ed25" + } + } + ], + "v":{ + "s":"ToAll" + }, + "i":"71a78b2a" + } + } +}""" } diff --git a/src/commonTest/kotlin/com/configcat/integration/matrix/UnicodeMatrix.kt b/src/commonTest/kotlin/com/configcat/integration/matrix/UnicodeMatrix.kt new file mode 100644 index 00000000..228c9942 --- /dev/null +++ b/src/commonTest/kotlin/com/configcat/integration/matrix/UnicodeMatrix.kt @@ -0,0 +1,676 @@ +package com.configcat.integration.matrix + +object UnicodeMatrix : DataMatrix { + override val sdkKey: String = "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/Da6w8dBbmUeMUBhh0iEeQQ" + override val data = + """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""" + override val remoteJson = + """{ + "p":{ + "u":"https://cdn-global.configcat.com", + "r":0, + "s":"V25d\u002Bbk2NfkY\u002Bq0Gle0pM361WEHJzwyWQ\u002BcSfdVs9hQ=" + }, + "f":{ + "boolArrayContainsCleartext":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"\uD83C\uDD83\uD83C\uDD74\uD83C\uDD87\uD83C\uDD83", + "c":34, + "l":[ + "ÁRVÍZTŰRŐ tükörfúrógép", + "u\uD835\uDD93\uD835\uDD8E\uD835\uDD88\uD835\uDD94\uD835\uDD89e" + ] + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"d7581d1d" + } + } + ], + "v":{ + "b":false + }, + "i":"ab278c56" + }, + "boolArrayContainsHashed":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"\uD83C\uDD83\uD83C\uDD74\uD83C\uDD87\uD83C\uDD83", + "c":26, + "l":[ + "c562d4492396a997352aeae7187b9c9f5e73798a8d3681da68a27b6997cb3763", + "4641c3569ae7d23b5907409ce15d906b871aa343e40a2cc2e58c82b4c786f295" + ] + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"3dbafdf8" + } + } + ], + "v":{ + "b":false + }, + "i":"351da900" + }, + "boolArrayNotContainsCleartext":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"\uD83C\uDD83\uD83C\uDD74\uD83C\uDD87\uD83C\uDD83", + "c":35, + "l":[ + "ÁRVÍZTŰRŐ tükörfúrógép", + "u\uD835\uDD93\uD835\uDD8E\uD835\uDD88\uD835\uDD94\uD835\uDD89e" + ] + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"4e2a9e02" + } + } + ], + "v":{ + "b":false + }, + "i":"e60bde32" + }, + "boolArrayNotContainsHashed":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"\uD83C\uDD83\uD83C\uDD74\uD83C\uDD87\uD83C\uDD83", + "c":27, + "l":[ + "09372ae681c13ba4ae65d8b22461173e0f26ccf808709cedb5bcee0db2ca2beb", + "4e5d15f7644d53aa4febc85c682026fcbbb10e972f0eb5991a4a57c4ad19dfc3" + ] + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"c434fcb9" + } + } + ], + "v":{ + "b":false + }, + "i":"fec0125d" + }, + "boolContainsCleartext":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"\uD83C\uDD83\uD83C\uDD74\uD83C\uDD87\uD83C\uDD83", + "c":2, + "l":[ + "ÁRVÍZTŰRŐ", + "tükörfúrógép", + "u\uD835\uDD93\uD835\uDD8E", + "\uD835\uDD88\uD835\uDD94\uD835\uDD89e" + ] + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"1b0c7055" + } + } + ], + "v":{ + "b":false + }, + "i":"9b855029" + }, + "boolEndsWithCleartext":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"\uD83C\uDD83\uD83C\uDD74\uD83C\uDD87\uD83C\uDD83", + "c":32, + "l":[ + "ÁRVÍZTŰRŐ", + "tükörfúrógép", + "u\uD835\uDD93\uD835\uDD8E", + "\uD835\uDD88\uD835\uDD94\uD835\uDD89e" + ] + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"968cc630" + } + } + ], + "v":{ + "b":false + }, + "i":"32e831fc" + }, + "boolEndsWithHashed":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"\uD83C\uDD83\uD83C\uDD74\uD83C\uDD87\uD83C\uDD83", + "c":24, + "l":[ + "13_950274ee38ff82f00ad5b9e3c5b7ef67d6999efc9062de88cc8d4a288c74ab00", + "17_4f87fcc086e15c043caadc8531864615426b5b4fe105626f0da2659f4bcc7683", + "9_6645286b3254cb3e067cd6b6e20f099e010d2e0b4c1f1830b8aee4ea24e77efc", + "13_0e82c292cf1bc116506703deadefd93f7436792e5c459f032c8030fc840ea66c" + ] + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"d0f8c4cc" + } + } + ], + "v":{ + "b":false + }, + "i":"582d003c" + }, + "boolIsNotOneOfCleartext":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"\uD83C\uDD83\uD83C\uDD74\uD83C\uDD87\uD83C\uDD83", + "c":1, + "l":[ + "ÁRVÍZTŰRŐ tükörfúrógép", + "u\uD835\uDD93\uD835\uDD8E\uD835\uDD88\uD835\uDD94\uD835\uDD89e" + ] + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"d507e0d2" + } + } + ], + "v":{ + "b":false + }, + "i":"b240d07d" + }, + "boolIsNotOneOfHashed":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"\uD83C\uDD83\uD83C\uDD74\uD83C\uDD87\uD83C\uDD83", + "c":17, + "l":[ + "f3802db2e5f5f3416f57106d9bfe4a1748f5fe53f6dce33c0182cc753ff90cf6", + "0e783d5dd44d6d234f47438b69641e7393fc90cd7e10faa38453f95b1d6547b9" + ] + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"4c287ac1" + } + } + ], + "v":{ + "b":false + }, + "i":"511ebd2c" + }, + "boolIsOneOfCleartext":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"\uD83C\uDD83\uD83C\uDD74\uD83C\uDD87\uD83C\uDD83", + "c":0, + "l":[ + "ÁRVÍZTŰRŐ tükörfúrógép", + "u\uD835\uDD93\uD835\uDD8E\uD835\uDD88\uD835\uDD94\uD835\uDD89e" + ] + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"7267e267" + } + } + ], + "v":{ + "b":false + }, + "i":"a784f049" + }, + "boolIsOneOfHashed":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"\uD83C\uDD83\uD83C\uDD74\uD83C\uDD87\uD83C\uDD83", + "c":16, + "l":[ + "eccc74e9fb25cec6db6883fa28ddbb470d3b088fc2667798cb80e42945f3af64", + "585d3de5e9ddd535cf1b3b69642a45ea96677eea3b01dc8b51a68df1dc8fcbd6" + ] + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"e1869f25" + } + } + ], + "v":{ + "b":false + }, + "i":"2ca5d4f6" + }, + "boolNotContainsCleartext":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"\uD83C\uDD83\uD83C\uDD74\uD83C\uDD87\uD83C\uDD83", + "c":3, + "l":[ + "ÁRVÍZTŰRŐ", + "tükörfúrógép", + "u\uD835\uDD93\uD835\uDD8E", + "\uD835\uDD88\uD835\uDD94\uD835\uDD89e" + ] + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"10540ea2" + } + } + ], + "v":{ + "b":false + }, + "i":"a0746844" + }, + "boolNotEndsWithCleartext":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"\uD83C\uDD83\uD83C\uDD74\uD83C\uDD87\uD83C\uDD83", + "c":33, + "l":[ + "ÁRVÍZTŰRŐ", + "tükörfúrógép", + "u\uD835\uDD93\uD835\uDD8E", + "\uD835\uDD88\uD835\uDD94\uD835\uDD89e" + ] + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"13185229" + } + } + ], + "v":{ + "b":false + }, + "i":"ea6e6660" + }, + "boolNotEndsWithHashed":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"\uD83C\uDD83\uD83C\uDD74\uD83C\uDD87\uD83C\uDD83", + "c":25, + "l":[ + "13_8728453fa52044c6b94bf26bb5f867a35dc2f0f903cad3dbddf856b84d6d4f46", + "17_5e72011508c9ee9bb7e07b2f014f5d521e26f86e82a620802e78328ba7ee6bee", + "9_100aacb55aefcd99af3926a48255e61982ca252f747f18a133c7ced0b3b6351e", + "13_d5dbcf0d6c2771a5059c89da62f6828a497ab0d3e010a0e52dd7cf339d3a9a50" + ] + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"268dd59d" + } + } + ], + "v":{ + "b":false + }, + "i":"0264ab4c" + }, + "boolNotStartsWithCleartext":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"\uD83C\uDD83\uD83C\uDD74\uD83C\uDD87\uD83C\uDD83", + "c":31, + "l":[ + "ÁRVÍZTŰRŐ", + "tükörfúrógép", + "u\uD835\uDD93\uD835\uDD8E", + "\uD835\uDD88\uD835\uDD94\uD835\uDD89e" + ] + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"cefae65b" + } + } + ], + "v":{ + "b":false + }, + "i":"c66b8227" + }, + "boolNotStartsWithHashed":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"\uD83C\uDD83\uD83C\uDD74\uD83C\uDD87\uD83C\uDD83", + "c":23, + "l":[ + "13_e684a44ab005250167a45d4b6e43c8131c02af075bd6d1acd67a9dba488cf1a9", + "17_e12051ab65d6ce71c027116cf7d8c47bdbcbddccc0960b83774265871f77885e", + "9_d10a8565bb05a756315f429def3f73de18e44c09ae3eeadaecffb9ff369bd0f1", + "13_da24e3e3d5784aadd3d260173c1a218ebf9d3d00c83a301f131d1bf0128ea1ab" + ] + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"cddfa2af" + } + } + ], + "v":{ + "b":false + }, + "i":"6ed7179f" + }, + "boolStartsWithCleartext":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"\uD83C\uDD83\uD83C\uDD74\uD83C\uDD87\uD83C\uDD83", + "c":30, + "l":[ + "ÁRVÍZTŰRŐ", + "tükörfúrógép", + "u\uD835\uDD93\uD835\uDD8E", + "\uD835\uDD88\uD835\uDD94\uD835\uDD89e" + ] + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"a92d0998" + } + } + ], + "v":{ + "b":false + }, + "i":"fb61ec46" + }, + "boolStartsWithHashed":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"\uD83C\uDD83\uD83C\uDD74\uD83C\uDD87\uD83C\uDD83", + "c":22, + "l":[ + "13_18c344560e31a88013112f486a0a7ff3449af2385dbb62d7f63f16dfe585f35a", + "17_50879525d9d9ad528d904640340d8e59be77d01a16a6a88a04fb69df26164a67", + "9_5562cd5ac3d55ae55721086916db054c5dcfd1029685861f3cfb7a7ccd601729", + "13_400a0593a70d31f129473b0a9fdab090677d61cd1993ca736bf749135a080423" + ] + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"694c5376" + } + } + ], + "v":{ + "b":false + }, + "i":"8ece2cfc" + }, + "boolTextEqualsCleartext":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"\uD83C\uDD83\uD83C\uDD74\uD83C\uDD87\uD83C\uDD83", + "c":28, + "s":"ʄǟռƈʏ ȶɛӼȶ" + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"fc2c6a61" + } + } + ], + "v":{ + "b":false + }, + "i":"a86779a6" + }, + "boolTextEqualsHashed":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"\uD83C\uDD83\uD83C\uDD74\uD83C\uDD87\uD83C\uDD83", + "c":20, + "s":"8d08b67cbb7dd65798ca58873eaf25f74a4aba7d6878e83b9d4c3d54afb311e7" + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"e7c0d232" + } + } + ], + "v":{ + "b":false + }, + "i":"4079953c" + }, + "boolTextNotEqualsCleartext":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"\uD83C\uDD83\uD83C\uDD74\uD83C\uDD87\uD83C\uDD83", + "c":29, + "s":"ʄǟռƈʏ ȶɛӼȶ" + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"2fc8ce05" + } + } + ], + "v":{ + "b":false + }, + "i":"d259a169" + }, + "boolTextNotEqualsHashed":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"\uD83C\uDD83\uD83C\uDD74\uD83C\uDD87\uD83C\uDD83", + "c":21, + "s":"6df3551476d9f5dc9a82e9eb861db949555a1e195180a44560e7c3ff70e19320" + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"fe477fc6" + } + } + ], + "v":{ + "b":false + }, + "i":"5252b4c2" + } + } +}""" +} diff --git a/src/commonTest/kotlin/com/configcat/integration/matrix/VariationIdMatrix.kt b/src/commonTest/kotlin/com/configcat/integration/matrix/VariationIdMatrix.kt index 09ef8aba..a6d72e18 100644 --- a/src/commonTest/kotlin/com/configcat/integration/matrix/VariationIdMatrix.kt +++ b/src/commonTest/kotlin/com/configcat/integration/matrix/VariationIdMatrix.kt @@ -1,7 +1,7 @@ package com.configcat.integration.matrix object VariationIdMatrix : DataMatrix { - override val sdkKey = "PKDVCLf-Hq-h-kCzMp-L7Q/nQ5qkhRAUEa6beEyyrVLBA" + override val sdkKey: String = "configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/spQnkRTIPEWVivZkWM84lQ" override val data = """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; @@ -11,5 +11,246 @@ 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;""" override val remoteJson = - """{"p":{"u":"https://cdn-global.configcat.com","r":0},"f":{"boolean":{"v":false,"i":"a0e56eda","t":0,"p":[{"o":0,"v":true,"p":50,"i":"67787ae4"},{"o":1,"v":false,"p":50,"i":"a0e56eda"}],"r":[{"o":0,"a":"Email","t":2,"c":"@configcat.com","v":true,"i":"67787ae4"}]},"text":{"v":"c","t":1,"i":"3f05be89","p":[{"o":0,"v":"a","p":50,"i":"30ba32b9"},{"o":1,"v":"b","p":50,"i":"cf19e913"}],"r":[{"o":0,"a":"Email","t":2,"c":"@configcat.com","v":"true","i":"9bdc6a1f"},{"o":1,"a":"Email","t":2,"c":"@test.com","v":"false","i":"65310deb"}]},"whole":{"v":999999,"i":"cf2e9162","t":2,"p":[{"o":0,"v":0,"p":50,"i":"ec14f6a9"},{"o":1,"v":-1,"p":50,"i":"61a5a033"}],"r":[{"o":0,"a":"Email","t":2,"c":"@configcat.com","v":1,"i":"ab30533b"}]},"decimal":{"v":0.0,"i":"63612d39","t":3,"p":[{"o":0,"v":1.0,"p":50,"i":"d0dbc27f"},{"o":1,"v":2.0,"p":50,"i":"8155ad7b"}],"r":[{"o":0,"a":"Email","t":2,"c":"@configcat.com","v":-2147483647.2147484,"i":"8f9559cf"},{"o":1,"a":"Email","t":0,"c":"a@test.com","v":0.12345678912345678,"i":"d66c5781"},{"o":2,"a":"Email","t":0,"c":"b@test.com","v":0.12345678912,"i":"d66c5781"}]}}}""" + """{ + "p":{ + "u":"https://cdn-global.configcat.com", + "r":0, + "s":"w2mxWDHVP0tEZ3jgEie61YF\u002BWwbZgsfJgLZN7UoiTIU=" + }, + "f":{ + "boolean":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"67787ae4" + } + } + ], + "p":[ + { + "p":50, + "v":{ + "b":true + }, + "i":"67787ae4" + }, + { + "p":50, + "v":{ + "b":false + }, + "i":"a0e56eda" + } + ], + "v":{ + "b":false + }, + "i":"a0e56eda" + }, + "decimal":{ + "t":3, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "d":-2147483647.2147484 + }, + "i":"8f9559cf" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":0, + "l":[ + "a@test.com" + ] + } + } + ], + "s":{ + "v":{ + "d":0.12345678912345678 + }, + "i":"d66c5781" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":0, + "l":[ + "b@test.com" + ] + } + } + ], + "s":{ + "v":{ + "d":0.12345678912 + }, + "i":"d66c5781" + } + } + ], + "p":[ + { + "p":50, + "v":{ + "d":1 + }, + "i":"d0dbc27f" + }, + { + "p":50, + "v":{ + "d":2 + }, + "i":"8155ad7b" + } + ], + "v":{ + "d":0 + }, + "i":"63612d39" + }, + "text":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"true" + }, + "i":"9bdc6a1f" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@test.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"false" + }, + "i":"65310deb" + } + } + ], + "p":[ + { + "p":50, + "v":{ + "s":"a" + }, + "i":"30ba32b9" + }, + { + "p":50, + "v":{ + "s":"b" + }, + "i":"cf19e913" + } + ], + "v":{ + "s":"c" + }, + "i":"3f05be89" + }, + "whole":{ + "t":2, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "i":1 + }, + "i":"ab30533b" + } + } + ], + "p":[ + { + "p":50, + "v":{ + "i":0 + }, + "i":"ec14f6a9" + }, + { + "p":50, + "v":{ + "i":-1 + }, + "i":"61a5a033" + } + ], + "v":{ + "i":999999 + }, + "i":"cf2e9162" + } + } +}""" } diff --git a/src/commonTest/kotlin/com/configcat/userattribute/UserAttributeConvertTest.kt b/src/commonTest/kotlin/com/configcat/userattribute/UserAttributeConvertTest.kt new file mode 100644 index 00000000..360c8af4 --- /dev/null +++ b/src/commonTest/kotlin/com/configcat/userattribute/UserAttributeConvertTest.kt @@ -0,0 +1,132 @@ +package com.configcat.userattribute + +import com.configcat.ConfigCatClient +import com.configcat.ConfigCatUser +import com.configcat.getValue +import com.configcat.log.LogLevel +import com.configcat.manualPoll +import com.configcat.userattribute.data.* +import com.soywiz.klock.DateTime +import io.ktor.client.engine.mock.* +import io.ktor.http.* +import io.ktor.util.date.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals + +@OptIn(ExperimentalCoroutinesApi::class) +class UserAttributeConvertTest { + + @Test + fun testStringConvert() = runTest { + runConvertTest(StringConvertData, 42, true) + } + + @Test + fun testSemverConvert() = runTest { + runConvertTest(SemVerConvertData, "0.0", "20%") + runConvertTest(SemVerConvertData, "0.9.9", "< 1.0.0") + runConvertTest(SemVerConvertData, "1.0.0", "20%") + runConvertTest(SemVerConvertData, "1.1", "20%") + runConvertTest(SemVerConvertData, 0, "20%") + runConvertTest(SemVerConvertData, 0.9, "20%") + runConvertTest(SemVerConvertData, 2, "20%") + } + + @Test + fun testStringArrayConvert() = runTest { + runConvertTest(StringArrayConvertData, arrayOf("x", "read"), "Dog") + runConvertTest(StringArrayConvertData, arrayOf("x", "Read"), "Cat") + runConvertTest(StringArrayConvertData, mutableListOf("x", "read"), "Dog") + runConvertTest(StringArrayConvertData, mutableListOf("x", "Read"), "Cat") + runConvertTest(StringArrayConvertData, "[\"x\", \"read\"]", "Dog") + runConvertTest(StringArrayConvertData, "[\"x\", \"Read\"]", "Cat") + runConvertTest(StringArrayConvertData, "x, read", "Cat") + } + + @Test + fun testDateConvert() = runTest { + runConvertTest(DateConvertData, "1680307200.001", true) + runConvertTest(DateConvertData, "1680307199.999", false) + runConvertTest(DateConvertData, 1680307201L, true) + runConvertTest(DateConvertData, 1680307199L, false) + runConvertTest(DateConvertData, 1680307200.001, true) + runConvertTest(DateConvertData, 1680307199.999, false) + runConvertTest(DateConvertData, DateTime(1680307200001L), true) + runConvertTest(DateConvertData, DateTime(1680307199999L), false) + } + + @Test + fun testNumberConvert() = runTest { + runConvertTest(NumberConvertData, -1, "<2.1") + runConvertTest(NumberConvertData, 2, "<2.1") + runConvertTest(NumberConvertData, 3, "<>4.2") + runConvertTest(NumberConvertData, 5, ">=5") + runConvertTest(NumberConvertData, -1L, "<2.1") + runConvertTest(NumberConvertData, 2L, "<2.1") + runConvertTest(NumberConvertData, 3L, "<>4.2") + runConvertTest(NumberConvertData, 5L, ">=5") + runConvertTest(NumberConvertData, -1.0, "<2.1") + runConvertTest(NumberConvertData, 2.0, "<2.1") + runConvertTest(NumberConvertData, 3.0, "<>4.2") + runConvertTest(NumberConvertData, 5.0, ">=5") + runConvertTest(NumberConvertData, -1.0f, "<2.1") + runConvertTest(NumberConvertData, 2.0f, "<2.1") + runConvertTest(NumberConvertData, 3.0f, "<>4.2") + runConvertTest(NumberConvertData, 5.0f, ">=5") + runConvertTest(NumberConvertData, "-1.0", "<2.1") + runConvertTest(NumberConvertData, "2.0", "<2.1") + runConvertTest(NumberConvertData, "3.0", "<>4.2") + runConvertTest(NumberConvertData, "5.0", ">=5") + runConvertTest(NumberConvertData, "-1", "<2.1") + runConvertTest(NumberConvertData, "2", "<2.1") + runConvertTest(NumberConvertData, "3", "<>4.2") + runConvertTest(NumberConvertData, "5", ">=5") + runConvertTest(NumberConvertData, Double.NaN, "<>4.2") + runConvertTest(NumberConvertData, Double.POSITIVE_INFINITY, ">5") + runConvertTest(NumberConvertData, Double.NEGATIVE_INFINITY, "<2.1") + runConvertTest(NumberConvertData, Float.NaN, "<>4.2") + runConvertTest(NumberConvertData, Float.POSITIVE_INFINITY, ">5") + runConvertTest(NumberConvertData, Float.NEGATIVE_INFINITY, "<2.1") + runConvertTest(NumberConvertData, Long.MAX_VALUE, ">5") + runConvertTest(NumberConvertData, Long.MIN_VALUE, "<2.1") + runConvertTest(NumberConvertData, Int.MAX_VALUE, ">5") + runConvertTest(NumberConvertData, Int.MIN_VALUE, "<2.1") + runConvertTest(NumberConvertData, "NotANumber", "80%") + runConvertTest(NumberConvertData, "Infinity", ">5") + runConvertTest(NumberConvertData, "NaN", "<>4.2") + runConvertTest(NumberConvertData, "NaNa", "80%") + runConvertTest(NumberConvertData, (-1).toByte(), "<2.1") + runConvertTest(NumberConvertData, (2).toByte(), "<2.1") + runConvertTest(NumberConvertData, (3).toByte(), "<>4.2") + runConvertTest(NumberConvertData, (5).toByte(), ">=5") + runConvertTest(NumberConvertData, (-1).toShort(), "<2.1") + runConvertTest(NumberConvertData, (2).toShort(), "<2.1") + runConvertTest(NumberConvertData, (3).toShort(), "<>4.2") + runConvertTest(NumberConvertData, (5).toShort(), ">=5") + } + + private suspend fun runConvertTest(data: ConvertData, customAttributeValue: Any, expectedValue: Any) { + val mockEngine = MockEngine { + respond(content = data.remoteJson, status = HttpStatusCode.OK) + } + val client = ConfigCatClient(data.sdkKey) { + pollingMode = manualPoll() + httpEngine = mockEngine + logLevel = LogLevel.ERROR + } + client.forceRefresh() + + val customAttributes = mutableMapOf() + customAttributes["Custom1"] = customAttributeValue + + val configCatUser = ConfigCatUser(identifier = "12345", custom = customAttributes) + + val value = client.getAnyValue(key = data.flagKey, defaultValue = data.defaultValue, user = configCatUser) + + assertEquals(expectedValue, value) + + ConfigCatClient.closeAll() + } +} diff --git a/src/commonTest/kotlin/com/configcat/userattribute/data/ConvertData.kt b/src/commonTest/kotlin/com/configcat/userattribute/data/ConvertData.kt new file mode 100644 index 00000000..a80813d7 --- /dev/null +++ b/src/commonTest/kotlin/com/configcat/userattribute/data/ConvertData.kt @@ -0,0 +1,8 @@ +package com.configcat.userattribute.data + +interface ConvertData { + val sdkKey: String + val flagKey: String + val defaultValue: Any + val remoteJson: String +} diff --git a/src/commonTest/kotlin/com/configcat/userattribute/data/DateConvertData.kt b/src/commonTest/kotlin/com/configcat/userattribute/data/DateConvertData.kt new file mode 100644 index 00000000..e85ddc61 --- /dev/null +++ b/src/commonTest/kotlin/com/configcat/userattribute/data/DateConvertData.kt @@ -0,0 +1,1412 @@ +package com.configcat.userattribute.data + +object DateConvertData : ConvertData { + override val sdkKey = "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ" + override val flagKey = "boolTrueIn202304" + override val defaultValue = true + override val remoteJson = """ + { + "p":{ + "u":"https://cdn-global.configcat.com", + "r":0, + "s":"brLK\u002BZy5OUFPomdNB48Sgv3HwDy6gXQ5q1\u002BvXlUxXws=" + }, + "f":{ + "allinone":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":20, + "s":"83ee2cd874eeb7ed1fbde0f16b8915a830f6c5a8e697ac8236c24c482f856045" + } + }, + { + "u":{ + "a":"Email", + "c":21, + "s":"83ee2cd874eeb7ed1fbde0f16b8915a830f6c5a8e697ac8236c24c482f856045" + } + } + ], + "s":{ + "v":{ + "s":"1h" + }, + "i":"e3a79156" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":28, + "s":"joe@example.com" + } + }, + { + "u":{ + "a":"Email", + "c":29, + "s":"joe@example.com" + } + } + ], + "s":{ + "v":{ + "s":"1c" + }, + "i":"ed60451a" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":16, + "l":[ + "83ee2cd874eeb7ed1fbde0f16b8915a830f6c5a8e697ac8236c24c482f856045" + ] + } + }, + { + "u":{ + "a":"Email", + "c":17, + "l":[ + "83ee2cd874eeb7ed1fbde0f16b8915a830f6c5a8e697ac8236c24c482f856045" + ] + } + } + ], + "s":{ + "v":{ + "s":"2h" + }, + "i":"aa24b7a3" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":0, + "l":[ + "joe@example.com" + ] + } + }, + { + "u":{ + "a":"Email", + "c":1, + "l":[ + "joe@example.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"2c" + }, + "i":"d37425a1" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":22, + "l":[ + "4_00903bfce5fabef65d1aad7f82d014b192b92ab90375b6a763e4e5cae119d698" + ] + } + }, + { + "u":{ + "a":"Email", + "c":23, + "l":[ + "4_00903bfce5fabef65d1aad7f82d014b192b92ab90375b6a763e4e5cae119d698" + ] + } + } + ], + "s":{ + "v":{ + "s":"3h" + }, + "i":"5e6e0c6c" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":30, + "l":[ + "joe@" + ] + } + }, + { + "u":{ + "a":"Email", + "c":31, + "l":[ + "joe@" + ] + } + } + ], + "s":{ + "v":{ + "s":"3c" + }, + "i":"5f562a70" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "12_8a189744250da86dcc8bf3af94f49ffcb26746d76eb9838bad2133e6f76c189f" + ] + } + }, + { + "u":{ + "a":"Email", + "c":25, + "l":[ + "12_8a189744250da86dcc8bf3af94f49ffcb26746d76eb9838bad2133e6f76c189f" + ] + } + } + ], + "s":{ + "v":{ + "s":"4h" + }, + "i":"91b91d69" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":32, + "l":[ + "@example.com" + ] + } + }, + { + "u":{ + "a":"Email", + "c":33, + "l":[ + "@example.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"4c" + }, + "i":"4c80a977" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "e@e" + ] + } + }, + { + "u":{ + "a":"Email", + "c":3, + "l":[ + "e@e" + ] + } + } + ], + "s":{ + "v":{ + "s":"5" + }, + "i":"dd12c429" + } + }, + { + "c":[ + { + "u":{ + "a":"Version", + "c":4, + "l":[ + "1.0.0" + ] + } + }, + { + "u":{ + "a":"Version", + "c":5, + "l":[ + "1.0.0" + ] + } + } + ], + "s":{ + "v":{ + "s":"6" + }, + "i":"dba5d266" + } + }, + { + "c":[ + { + "u":{ + "a":"Version", + "c":6, + "s":"1.0.1" + } + }, + { + "u":{ + "a":"Version", + "c":9, + "s":"1.0.1" + } + } + ], + "s":{ + "v":{ + "s":"7" + }, + "i":"1637ffc5" + } + }, + { + "c":[ + { + "u":{ + "a":"Version", + "c":8, + "s":"0.9.9" + } + }, + { + "u":{ + "a":"Version", + "c":7, + "s":"0.9.9" + } + } + ], + "s":{ + "v":{ + "s":"8" + }, + "i":"b084ddd6" + } + }, + { + "c":[ + { + "u":{ + "a":"Number", + "c":10, + "d":1 + } + }, + { + "u":{ + "a":"Number", + "c":11, + "d":1 + } + } + ], + "s":{ + "v":{ + "s":"9" + }, + "i":"d1d537a6" + } + }, + { + "c":[ + { + "u":{ + "a":"Number", + "c":12, + "d":1.1 + } + }, + { + "u":{ + "a":"Number", + "c":15, + "d":1.1 + } + } + ], + "s":{ + "v":{ + "s":"10" + }, + "i":"52c846d0" + } + }, + { + "c":[ + { + "u":{ + "a":"Number", + "c":14, + "d":0.9 + } + }, + { + "u":{ + "a":"Number", + "c":13, + "d":0.9 + } + } + ], + "s":{ + "v":{ + "s":"11" + }, + "i":"c91ffb7c" + } + }, + { + "c":[ + { + "u":{ + "a":"Date", + "c":18, + "d":1693497600 + } + }, + { + "u":{ + "a":"Date", + "c":19, + "d":1693497600 + } + } + ], + "s":{ + "v":{ + "s":"12" + }, + "i":"c12182ef" + } + }, + { + "c":[ + { + "u":{ + "a":"Country", + "c":26, + "l":[ + "fe9a56922634f9b8492bda6a11e4db55d2042b7474260d5d07c46df65114ab3c" + ] + } + }, + { + "u":{ + "a":"Country", + "c":27, + "l":[ + "fe9a56922634f9b8492bda6a11e4db55d2042b7474260d5d07c46df65114ab3c" + ] + } + } + ], + "s":{ + "v":{ + "s":"13h" + }, + "i":"a16b1a17" + } + }, + { + "c":[ + { + "u":{ + "a":"Country", + "c":34, + "l":[ + "USA" + ] + } + }, + { + "u":{ + "a":"Country", + "c":35, + "l":[ + "USA" + ] + } + } + ], + "s":{ + "v":{ + "s":"13c" + }, + "i":"1a17d1b3" + } + } + ], + "v":{ + "s":"default" + }, + "i":"9ff25f81" + }, + "arrayContainsCaseCheckDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":26, + "l":[ + "11bcd95e7760685f2cdca35f137d05fb70b98ed62a7126697df474ec43fd9d62" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"5d80eff1" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"ce055a38" + }, + "arrayContainsDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":26, + "l":[ + "05fa8abeef965b893e455dfef83db94ef28f01b0ea14b4c1f71929fc2f725753" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"147fdd01" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"5f573f9c" + }, + "arrayDoesNotContainCaseCheckDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":27, + "l":[ + "980707686b0e1854fa318924d3ff7ebf8f74412a3799c6087f3e59bf71279869" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"d4ad5730" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"df4915fd" + }, + "arrayDoesNotContainDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":27, + "l":[ + "8656713f187cf1d3492b092a5ffd2207930893b3ca365226af048763d5ed5283" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"c2161ac9" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"41910880" + }, + "boolTextEqualsNumber":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":28, + "s":"42" + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"28228808" + } + } + ], + "v":{ + "b":false + }, + "i":"fe23db9c" + }, + "boolTrueIn202304":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":19, + "d":1680307200 + } + }, + { + "u":{ + "a":"Custom1", + "c":18, + "d":1682899200 + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"6948d7cd" + } + } + ], + "v":{ + "b":false + }, + "i":"ae2a09bd" + }, + "countryPercentageAttribute":{ + "t":1, + "a":"Country", + "p":[ + { + "p":50, + "v":{ + "s":"Falcon" + }, + "i":"2b05fd81" + }, + { + "p":50, + "v":{ + "s":"Horse" + }, + "i":"e28b6a82" + } + ], + "v":{ + "s":"Chicken" + }, + "i":"29bb6bbb" + }, + "customPercentageAttribute":{ + "t":1, + "a":"Custom1", + "p":[ + { + "p":50, + "v":{ + "s":"Falcon" + }, + "i":"3715712d" + }, + { + "p":50, + "v":{ + "s":"Horse" + }, + "i":"7b3542d5" + } + ], + "v":{ + "s":"Chicken" + }, + "i":"50466fb6" + }, + "missingPercentageAttribute":{ + "t":1, + "a":"NotFound", + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "14_73c8547dd9486c7c73707d1fdea4abefdd67b5c99b699e8fba112515608a186e" + ] + } + } + ], + "p":[ + { + "p":50, + "v":{ + "s":"Falcon" + }, + "i":"4b7d88ba" + }, + { + "p":50, + "v":{ + "s":"Horse" + }, + "i":"a1c2c9a9" + } + ] + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "14_73c8547dd9486c7c73707d1fdea4abefdd67b5c99b699e8fba112515608a186e" + ] + } + } + ], + "s":{ + "v":{ + "s":"NotFound" + }, + "i":"8aa042fe" + } + } + ], + "v":{ + "s":"Chicken" + }, + "i":"e5107172" + }, + "stringArrayContainsAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":34, + "l":[ + "read", + "write", + "execute" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"9ddb8a37" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"0d45ab4b" + }, + "stringArrayContainsAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":26, + "l":[ + "5ac671434b10e94fce1e30b1a81466e3820adb6e61db3fd191a9b5e3a21dfa92", + "5db65a4bfc7a0ab53515015fc9fdd834034e7e1ce56dfed3e563e9e3a5d85ff6", + "872971fa051302ed22add5b89c772679dd825f3f57281fc4f4d06894d4c2962c" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"aa03b1ff" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"203317f5" + }, + "stringArrayNotContainsAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":35, + "l":[ + "read", + "write", + "execute" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"15c865df" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"6df210da" + }, + "stringArrayNotContainsAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":27, + "l":[ + "d0b563571357fa8c4e984428637569ecc7c3506671632770567711f8b90cb368", + "f9ebea55bb55e515339510ec020dcbfbe942a7e907cef9ce6d1bb6fdd735d8f1", + "d98bd0b50ab3def5463d5ef743063433bc13b328149aaa959dfc0b3afd042868" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"259816ba" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"60b961b0" + }, + "stringContainsAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@verizon.com", + "@verizon.net", + "@aol.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"09af657f" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"063bcf39" + }, + "stringDoseNotEqualDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":21, + "s":"abd8ae2ea7aeb1f19c08e4c71a53c7c9a12d189964b38a99c73b6fdd6a3f8097" + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"8e423808" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"1835a09a" + }, + "stringEndsWithAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":32, + "l":[ + "@verizon.com", + "@verizon.net", + "@aol.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"33d35402" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"31976ec3" + }, + "stringEndsWithAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "12_80b1d3bc456925165de90add33dc23e2561c4e4cde5f52bd47fcfaeadd915eab", + "12_beb9d6142d7a6f3c72f52d79769022c1cf27610b6208a86712a25cd36dc2778c", + "8_bf64c8d66c84e43e226c2b8f59cf74a9616acddddb18d9cbd8ecc12711df52a8" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"7231ddf8" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"de17fd2a" + }, + "stringEndsWithDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "14_8c1ebc832cfbeedf2b18e9c3041248164f9c60be1f3374d3884604f2b20d8aa3" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"d7a00741" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"45b7d922" + }, + "stringEqualsCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":28, + "s":"a@configcat.com" + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"087e01dd" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"89785ab3" + }, + "stringEqualsDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":20, + "s":"3fd1f9f6da935590c3e5fce1e8c1182a80f7ad649db89a546651c4d505ef8522" + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"703c31ed" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"adc0b01c" + }, + "stringNotContainsAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":3, + "l":[ + "@verizon.com", + "@verizon.net", + "@aol.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"49627b36" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"36848b03" + }, + "stringNotEndsWithAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":33, + "l":[ + "@verizon.com", + "@verizon.net", + "@aol.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"886ced9d" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"864b6202" + }, + "stringNotEndsWithAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":25, + "l":[ + "12_fb5e2ce7ba0191194782c183ab7b63995790f6ebf148b8fbb94407f656cc9126", + "12_58bb59d19771d0197cdcd993c516df9564dcfb31a0e61fa4d43c91dc0c7c7729", + "8_e6bf34ddc03127d516fa6193168276ce3bfcb9c964786b52ce753ba5eb4e6c65" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"6eb0ec3a" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"7020bcd6" + }, + "stringNotEndsWithDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":25, + "l":[ + "14_9d758cb8cadc3493f7f56e505e81e28cd26c4fa0b0913b1db205f27855345d00" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"d37b6f18" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"91ba1bcb" + }, + "stringNotEqualsCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":29, + "s":"b@configcat.com" + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"3ace20fb" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"09c9725f" + }, + "stringNotStartsWithAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":31, + "l":[ + "u@", + "a@", + "b@" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"3717f2ca" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"1d661433" + }, + "stringNotStartsWithAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":23, + "l":[ + "2_2c317623f35bffec808ccaed2a3f29c7a78ab2276d6821ae17e7e22b4c7ecb1d", + "2_3f1f139ea0136aee360ae7c35a3e9450ce6796c8e1b0855c27436148ed345dc2", + "2_80d7473697ee1d8259959951fe17947cfda43c5f3cee8c17f439c04bab5606ef" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"b5ba025e" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"c35929e3" + }, + "stringNotStartsWithDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":23, + "l":[ + "1_4708693c9a610590f6e487a659a6f7e7fb6509ed2c158a5299fdef585115dd55" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"72c4e1ac" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"2b16da78" + }, + "stringStartsWithAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":30, + "l":[ + "u@", + "a@", + "b@" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"9e55f5cf" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"e170a185" + }, + "stringStartsWithAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":22, + "l":[ + "2_3a9dccacd7da15be19e54f7bca31fbe951fed6f6c2fab00fb8cae9f3316ca77f", + "2_6259e0c23524e7655dbf66e2b3aedc4186f26f594a700b3bc879928a226f5db7", + "2_734e111f23bd3a6fa57dfe234682cb70225d120911804b37ea03f25343db330f" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"1d9b7603" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"dd5b3211" + }, + "stringStartsWithDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":22, + "l":[ + "1_4e489426d4434d93128c8fceadbbb040d109874cb5d5fbe29fc8df5ad37284ea" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"3b409872" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"3659b0fe" + } + } + } + """.trimIndent() +} diff --git a/src/commonTest/kotlin/com/configcat/userattribute/data/NumberConvertData.kt b/src/commonTest/kotlin/com/configcat/userattribute/data/NumberConvertData.kt new file mode 100644 index 00000000..2d5a1f92 --- /dev/null +++ b/src/commonTest/kotlin/com/configcat/userattribute/data/NumberConvertData.kt @@ -0,0 +1,171 @@ +package com.configcat.userattribute.data + +object NumberConvertData : ConvertData { + override val sdkKey = "configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw" + override val flagKey = "numberWithPercentage" + override val defaultValue = "" + override val remoteJson = """ + { + "p":{ + "u":"https://cdn-global.configcat.com", + "r":0, + "s":"9TrpLBc00y4tHZOtN7VLBJS25JkNk8Y4FaqRwqYfNCs=" + }, + "f":{ + "number":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":11, + "d":5 + } + } + ], + "s":{ + "v":{ + "s":"\u003C\u003E5" + }, + "i":"a41938c5" + } + } + ], + "v":{ + "s":"Default" + }, + "i":"5ced27a9" + }, + "numberWithPercentage":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":12, + "d":2.1 + } + } + ], + "s":{ + "v":{ + "s":"\u003C2.1" + }, + "i":"a900bc23" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":13, + "d":2.1 + } + } + ], + "s":{ + "v":{ + "s":"\u003C=2,1" + }, + "i":"2c85f73d" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":10, + "d":3.5 + } + } + ], + "s":{ + "v":{ + "s":"=3.5" + }, + "i":"ae86baf5" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":14, + "d":5 + } + } + ], + "s":{ + "v":{ + "s":"\u003E5" + }, + "i":"c6924001" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":15, + "d":5 + } + } + ], + "s":{ + "v":{ + "s":"\u003E=5" + }, + "i":"8090543a" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":11, + "d":4.2 + } + } + ], + "s":{ + "v":{ + "s":"\u003C\u003E4.2" + }, + "i":"2691fade" + } + } + ], + "p":[ + { + "p":80, + "v":{ + "s":"80%" + }, + "i":"ad5f05a7" + }, + { + "p":20, + "v":{ + "s":"20%" + }, + "i":"786b696f" + } + ], + "v":{ + "s":"Default" + }, + "i":"642bbb26" + } + } + } + """.trimIndent() +} diff --git a/src/commonTest/kotlin/com/configcat/userattribute/data/SemVerConvertData.kt b/src/commonTest/kotlin/com/configcat/userattribute/data/SemVerConvertData.kt new file mode 100644 index 00000000..04d741ed --- /dev/null +++ b/src/commonTest/kotlin/com/configcat/userattribute/data/SemVerConvertData.kt @@ -0,0 +1,500 @@ +package com.configcat.userattribute.data + +object SemVerConvertData : ConvertData { + override val sdkKey = "configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/iV8vH2MBakKxkFZylxHmTg" + override val flagKey = "lessThanWithPercentage" + override val defaultValue = "" + override val remoteJson = """ + { + "p":{ + "u":"https://cdn-global.configcat.com", + "r":0, + "s":"tLxAdvIlYZSfBt2\u002BSnMVvx8B34uHxU5WspW/621ia10=" + }, + "f":{ + "isNotOneOf":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":5, + "l":[ + "1.0.0", + "1.0.1", + "2.0.0", + "2.0.1", + "2.0.2" + ] + } + } + ], + "s":{ + "v":{ + "s":"Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, )" + }, + "i":"a8d5f278" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":5, + "l":[ + "1.0.0", + "3.0.1" + ] + } + } + ], + "s":{ + "v":{ + "s":"Is not one of (1.0.0, 3.0.1)" + }, + "i":"54ac757f" + } + } + ], + "v":{ + "s":"Default" + }, + "i":"f79b763d" + }, + "isNotOneOfWithPercentage":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":5, + "l":[ + "1.0.0", + "1.0.1", + "2.0.0", + "2.0.1", + "2.0.2" + ] + } + } + ], + "s":{ + "v":{ + "s":"Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, )" + }, + "i":"9bf9e66f" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":5, + "l":[ + "1.0.0", + "3.0.1" + ] + } + } + ], + "s":{ + "v":{ + "s":"Is not one of (1.0.0, 3.0.1)" + }, + "i":"bfc1a544" + } + } + ], + "p":[ + { + "p":20, + "v":{ + "s":"20%" + }, + "i":"68f652f0" + }, + { + "p":80, + "v":{ + "s":"80%" + }, + "i":"b8d926e0" + } + ], + "v":{ + "s":"Default" + }, + "i":"b9614bad" + }, + "isOneOf":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":4, + "l":[ + "1.0.0", + "2" + ] + } + } + ], + "s":{ + "v":{ + "s":"Is one of (1.0.0, 2)" + }, + "i":"1e934047" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":4, + "l":[ + "1.0.0" + ] + } + } + ], + "s":{ + "v":{ + "s":"Is one of (1.0.0)" + }, + "i":"44342254" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":4, + "l":[ + "2.0.1", + "2.0.2" + ] + } + } + ], + "s":{ + "v":{ + "s":"Is one of ( , 2.0.1, 2.0.2, )" + }, + "i":"90e3ef46" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":4, + "l":[ + "3......" + ] + } + } + ], + "s":{ + "v":{ + "s":"Is one of (3......)" + }, + "i":"59523971" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":4, + "l":[ + "3...." + ] + } + } + ], + "s":{ + "v":{ + "s":"Is one of (3...)" + }, + "i":"2de217a1" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":4, + "l":[ + "3..0" + ] + } + } + ], + "s":{ + "v":{ + "s":"Is one of (3..0)" + }, + "i":"bf943c79" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":4, + "l":[ + "3.0" + ] + } + } + ], + "s":{ + "v":{ + "s":"Is one of (3.0)" + }, + "i":"3a6a8077" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":4, + "l":[ + "3.0." + ] + } + } + ], + "s":{ + "v":{ + "s":"Is one of (3.0.)" + }, + "i":"44f25fed" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":4, + "l":[ + "3.0.0" + ] + } + } + ], + "s":{ + "v":{ + "s":"Is one of (3.0.0)" + }, + "i":"e77f5306" + } + } + ], + "v":{ + "s":"Default" + }, + "i":"c4ec4d53" + }, + "isOneOfWithPercentage":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":4, + "l":[ + "1.0.0" + ] + } + } + ], + "s":{ + "v":{ + "s":"is one of (1.0.0)" + }, + "i":"0ac4afc1" + } + } + ], + "p":[ + { + "p":20, + "v":{ + "s":"20%" + }, + "i":"e25dba31" + }, + { + "p":80, + "v":{ + "s":"80%" + }, + "i":"8c70c181" + } + ], + "v":{ + "s":"Default" + }, + "i":"a94ff896" + }, + "lessThanWithPercentage":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":6, + "s":"1.0.0" + } + } + ], + "s":{ + "v":{ + "s":"\u003C 1.0.0" + }, + "i":"0c27d053" + } + } + ], + "p":[ + { + "p":20, + "v":{ + "s":"20%" + }, + "i":"3b1fde2a" + }, + { + "p":80, + "v":{ + "s":"80%" + }, + "i":"42e92759" + } + ], + "v":{ + "s":"Default" + }, + "i":"0081c525" + }, + "relations":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":6, + "s":"1.0.0," + } + } + ], + "s":{ + "v":{ + "s":"\u003C1.0.0," + }, + "i":"21b31b61" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":6, + "s":"1.0.0" + } + } + ], + "s":{ + "v":{ + "s":"\u003C 1.0.0" + }, + "i":"db3ddb7d" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":7, + "s":"1.0.0" + } + } + ], + "s":{ + "v":{ + "s":"\u003C=1.0.0" + }, + "i":"aa2c7493" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":8, + "s":"2.0.0" + } + } + ], + "s":{ + "v":{ + "s":"\u003E2.0.0" + }, + "i":"5e47a1ea" + } + }, + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":9, + "s":"2.0.0" + } + } + ], + "s":{ + "v":{ + "s":"\u003E=2.0.0" + }, + "i":"99482756" + } + } + ], + "v":{ + "s":"Default" + }, + "i":"c6155773" + } + } + } + """.trimIndent() +} diff --git a/src/commonTest/kotlin/com/configcat/userattribute/data/StringArrayConvertData.kt b/src/commonTest/kotlin/com/configcat/userattribute/data/StringArrayConvertData.kt new file mode 100644 index 00000000..42211dac --- /dev/null +++ b/src/commonTest/kotlin/com/configcat/userattribute/data/StringArrayConvertData.kt @@ -0,0 +1,1412 @@ +package com.configcat.userattribute.data + +object StringArrayConvertData : ConvertData { + override val sdkKey = "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ" + override val flagKey = "stringArrayContainsAnyOfDogDefaultCat" + override val defaultValue = "" + override val remoteJson = """ + { + "p":{ + "u":"https://cdn-global.configcat.com", + "r":0, + "s":"brLK\u002BZy5OUFPomdNB48Sgv3HwDy6gXQ5q1\u002BvXlUxXws=" + }, + "f":{ + "allinone":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":20, + "s":"83ee2cd874eeb7ed1fbde0f16b8915a830f6c5a8e697ac8236c24c482f856045" + } + }, + { + "u":{ + "a":"Email", + "c":21, + "s":"83ee2cd874eeb7ed1fbde0f16b8915a830f6c5a8e697ac8236c24c482f856045" + } + } + ], + "s":{ + "v":{ + "s":"1h" + }, + "i":"e3a79156" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":28, + "s":"joe@example.com" + } + }, + { + "u":{ + "a":"Email", + "c":29, + "s":"joe@example.com" + } + } + ], + "s":{ + "v":{ + "s":"1c" + }, + "i":"ed60451a" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":16, + "l":[ + "83ee2cd874eeb7ed1fbde0f16b8915a830f6c5a8e697ac8236c24c482f856045" + ] + } + }, + { + "u":{ + "a":"Email", + "c":17, + "l":[ + "83ee2cd874eeb7ed1fbde0f16b8915a830f6c5a8e697ac8236c24c482f856045" + ] + } + } + ], + "s":{ + "v":{ + "s":"2h" + }, + "i":"aa24b7a3" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":0, + "l":[ + "joe@example.com" + ] + } + }, + { + "u":{ + "a":"Email", + "c":1, + "l":[ + "joe@example.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"2c" + }, + "i":"d37425a1" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":22, + "l":[ + "4_00903bfce5fabef65d1aad7f82d014b192b92ab90375b6a763e4e5cae119d698" + ] + } + }, + { + "u":{ + "a":"Email", + "c":23, + "l":[ + "4_00903bfce5fabef65d1aad7f82d014b192b92ab90375b6a763e4e5cae119d698" + ] + } + } + ], + "s":{ + "v":{ + "s":"3h" + }, + "i":"5e6e0c6c" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":30, + "l":[ + "joe@" + ] + } + }, + { + "u":{ + "a":"Email", + "c":31, + "l":[ + "joe@" + ] + } + } + ], + "s":{ + "v":{ + "s":"3c" + }, + "i":"5f562a70" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "12_8a189744250da86dcc8bf3af94f49ffcb26746d76eb9838bad2133e6f76c189f" + ] + } + }, + { + "u":{ + "a":"Email", + "c":25, + "l":[ + "12_8a189744250da86dcc8bf3af94f49ffcb26746d76eb9838bad2133e6f76c189f" + ] + } + } + ], + "s":{ + "v":{ + "s":"4h" + }, + "i":"91b91d69" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":32, + "l":[ + "@example.com" + ] + } + }, + { + "u":{ + "a":"Email", + "c":33, + "l":[ + "@example.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"4c" + }, + "i":"4c80a977" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "e@e" + ] + } + }, + { + "u":{ + "a":"Email", + "c":3, + "l":[ + "e@e" + ] + } + } + ], + "s":{ + "v":{ + "s":"5" + }, + "i":"dd12c429" + } + }, + { + "c":[ + { + "u":{ + "a":"Version", + "c":4, + "l":[ + "1.0.0" + ] + } + }, + { + "u":{ + "a":"Version", + "c":5, + "l":[ + "1.0.0" + ] + } + } + ], + "s":{ + "v":{ + "s":"6" + }, + "i":"dba5d266" + } + }, + { + "c":[ + { + "u":{ + "a":"Version", + "c":6, + "s":"1.0.1" + } + }, + { + "u":{ + "a":"Version", + "c":9, + "s":"1.0.1" + } + } + ], + "s":{ + "v":{ + "s":"7" + }, + "i":"1637ffc5" + } + }, + { + "c":[ + { + "u":{ + "a":"Version", + "c":8, + "s":"0.9.9" + } + }, + { + "u":{ + "a":"Version", + "c":7, + "s":"0.9.9" + } + } + ], + "s":{ + "v":{ + "s":"8" + }, + "i":"b084ddd6" + } + }, + { + "c":[ + { + "u":{ + "a":"Number", + "c":10, + "d":1 + } + }, + { + "u":{ + "a":"Number", + "c":11, + "d":1 + } + } + ], + "s":{ + "v":{ + "s":"9" + }, + "i":"d1d537a6" + } + }, + { + "c":[ + { + "u":{ + "a":"Number", + "c":12, + "d":1.1 + } + }, + { + "u":{ + "a":"Number", + "c":15, + "d":1.1 + } + } + ], + "s":{ + "v":{ + "s":"10" + }, + "i":"52c846d0" + } + }, + { + "c":[ + { + "u":{ + "a":"Number", + "c":14, + "d":0.9 + } + }, + { + "u":{ + "a":"Number", + "c":13, + "d":0.9 + } + } + ], + "s":{ + "v":{ + "s":"11" + }, + "i":"c91ffb7c" + } + }, + { + "c":[ + { + "u":{ + "a":"Date", + "c":18, + "d":1693497600 + } + }, + { + "u":{ + "a":"Date", + "c":19, + "d":1693497600 + } + } + ], + "s":{ + "v":{ + "s":"12" + }, + "i":"c12182ef" + } + }, + { + "c":[ + { + "u":{ + "a":"Country", + "c":26, + "l":[ + "fe9a56922634f9b8492bda6a11e4db55d2042b7474260d5d07c46df65114ab3c" + ] + } + }, + { + "u":{ + "a":"Country", + "c":27, + "l":[ + "fe9a56922634f9b8492bda6a11e4db55d2042b7474260d5d07c46df65114ab3c" + ] + } + } + ], + "s":{ + "v":{ + "s":"13h" + }, + "i":"a16b1a17" + } + }, + { + "c":[ + { + "u":{ + "a":"Country", + "c":34, + "l":[ + "USA" + ] + } + }, + { + "u":{ + "a":"Country", + "c":35, + "l":[ + "USA" + ] + } + } + ], + "s":{ + "v":{ + "s":"13c" + }, + "i":"1a17d1b3" + } + } + ], + "v":{ + "s":"default" + }, + "i":"9ff25f81" + }, + "arrayContainsCaseCheckDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":26, + "l":[ + "11bcd95e7760685f2cdca35f137d05fb70b98ed62a7126697df474ec43fd9d62" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"5d80eff1" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"ce055a38" + }, + "arrayContainsDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":26, + "l":[ + "05fa8abeef965b893e455dfef83db94ef28f01b0ea14b4c1f71929fc2f725753" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"147fdd01" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"5f573f9c" + }, + "arrayDoesNotContainCaseCheckDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":27, + "l":[ + "980707686b0e1854fa318924d3ff7ebf8f74412a3799c6087f3e59bf71279869" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"d4ad5730" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"df4915fd" + }, + "arrayDoesNotContainDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":27, + "l":[ + "8656713f187cf1d3492b092a5ffd2207930893b3ca365226af048763d5ed5283" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"c2161ac9" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"41910880" + }, + "boolTextEqualsNumber":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":28, + "s":"42" + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"28228808" + } + } + ], + "v":{ + "b":false + }, + "i":"fe23db9c" + }, + "boolTrueIn202304":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":19, + "d":1680307200 + } + }, + { + "u":{ + "a":"Custom1", + "c":18, + "d":1682899200 + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"6948d7cd" + } + } + ], + "v":{ + "b":false + }, + "i":"ae2a09bd" + }, + "countryPercentageAttribute":{ + "t":1, + "a":"Country", + "p":[ + { + "p":50, + "v":{ + "s":"Falcon" + }, + "i":"2b05fd81" + }, + { + "p":50, + "v":{ + "s":"Horse" + }, + "i":"e28b6a82" + } + ], + "v":{ + "s":"Chicken" + }, + "i":"29bb6bbb" + }, + "customPercentageAttribute":{ + "t":1, + "a":"Custom1", + "p":[ + { + "p":50, + "v":{ + "s":"Falcon" + }, + "i":"3715712d" + }, + { + "p":50, + "v":{ + "s":"Horse" + }, + "i":"7b3542d5" + } + ], + "v":{ + "s":"Chicken" + }, + "i":"50466fb6" + }, + "missingPercentageAttribute":{ + "t":1, + "a":"NotFound", + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "14_73c8547dd9486c7c73707d1fdea4abefdd67b5c99b699e8fba112515608a186e" + ] + } + } + ], + "p":[ + { + "p":50, + "v":{ + "s":"Falcon" + }, + "i":"4b7d88ba" + }, + { + "p":50, + "v":{ + "s":"Horse" + }, + "i":"a1c2c9a9" + } + ] + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "14_73c8547dd9486c7c73707d1fdea4abefdd67b5c99b699e8fba112515608a186e" + ] + } + } + ], + "s":{ + "v":{ + "s":"NotFound" + }, + "i":"8aa042fe" + } + } + ], + "v":{ + "s":"Chicken" + }, + "i":"e5107172" + }, + "stringArrayContainsAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":34, + "l":[ + "read", + "write", + "execute" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"9ddb8a37" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"0d45ab4b" + }, + "stringArrayContainsAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":26, + "l":[ + "5ac671434b10e94fce1e30b1a81466e3820adb6e61db3fd191a9b5e3a21dfa92", + "5db65a4bfc7a0ab53515015fc9fdd834034e7e1ce56dfed3e563e9e3a5d85ff6", + "872971fa051302ed22add5b89c772679dd825f3f57281fc4f4d06894d4c2962c" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"aa03b1ff" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"203317f5" + }, + "stringArrayNotContainsAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":35, + "l":[ + "read", + "write", + "execute" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"15c865df" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"6df210da" + }, + "stringArrayNotContainsAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":27, + "l":[ + "d0b563571357fa8c4e984428637569ecc7c3506671632770567711f8b90cb368", + "f9ebea55bb55e515339510ec020dcbfbe942a7e907cef9ce6d1bb6fdd735d8f1", + "d98bd0b50ab3def5463d5ef743063433bc13b328149aaa959dfc0b3afd042868" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"259816ba" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"60b961b0" + }, + "stringContainsAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@verizon.com", + "@verizon.net", + "@aol.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"09af657f" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"063bcf39" + }, + "stringDoseNotEqualDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":21, + "s":"abd8ae2ea7aeb1f19c08e4c71a53c7c9a12d189964b38a99c73b6fdd6a3f8097" + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"8e423808" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"1835a09a" + }, + "stringEndsWithAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":32, + "l":[ + "@verizon.com", + "@verizon.net", + "@aol.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"33d35402" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"31976ec3" + }, + "stringEndsWithAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "12_80b1d3bc456925165de90add33dc23e2561c4e4cde5f52bd47fcfaeadd915eab", + "12_beb9d6142d7a6f3c72f52d79769022c1cf27610b6208a86712a25cd36dc2778c", + "8_bf64c8d66c84e43e226c2b8f59cf74a9616acddddb18d9cbd8ecc12711df52a8" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"7231ddf8" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"de17fd2a" + }, + "stringEndsWithDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "14_8c1ebc832cfbeedf2b18e9c3041248164f9c60be1f3374d3884604f2b20d8aa3" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"d7a00741" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"45b7d922" + }, + "stringEqualsCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":28, + "s":"a@configcat.com" + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"087e01dd" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"89785ab3" + }, + "stringEqualsDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":20, + "s":"3fd1f9f6da935590c3e5fce1e8c1182a80f7ad649db89a546651c4d505ef8522" + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"703c31ed" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"adc0b01c" + }, + "stringNotContainsAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":3, + "l":[ + "@verizon.com", + "@verizon.net", + "@aol.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"49627b36" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"36848b03" + }, + "stringNotEndsWithAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":33, + "l":[ + "@verizon.com", + "@verizon.net", + "@aol.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"886ced9d" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"864b6202" + }, + "stringNotEndsWithAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":25, + "l":[ + "12_fb5e2ce7ba0191194782c183ab7b63995790f6ebf148b8fbb94407f656cc9126", + "12_58bb59d19771d0197cdcd993c516df9564dcfb31a0e61fa4d43c91dc0c7c7729", + "8_e6bf34ddc03127d516fa6193168276ce3bfcb9c964786b52ce753ba5eb4e6c65" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"6eb0ec3a" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"7020bcd6" + }, + "stringNotEndsWithDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":25, + "l":[ + "14_9d758cb8cadc3493f7f56e505e81e28cd26c4fa0b0913b1db205f27855345d00" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"d37b6f18" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"91ba1bcb" + }, + "stringNotEqualsCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":29, + "s":"b@configcat.com" + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"3ace20fb" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"09c9725f" + }, + "stringNotStartsWithAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":31, + "l":[ + "u@", + "a@", + "b@" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"3717f2ca" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"1d661433" + }, + "stringNotStartsWithAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":23, + "l":[ + "2_2c317623f35bffec808ccaed2a3f29c7a78ab2276d6821ae17e7e22b4c7ecb1d", + "2_3f1f139ea0136aee360ae7c35a3e9450ce6796c8e1b0855c27436148ed345dc2", + "2_80d7473697ee1d8259959951fe17947cfda43c5f3cee8c17f439c04bab5606ef" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"b5ba025e" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"c35929e3" + }, + "stringNotStartsWithDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":23, + "l":[ + "1_4708693c9a610590f6e487a659a6f7e7fb6509ed2c158a5299fdef585115dd55" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"72c4e1ac" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"2b16da78" + }, + "stringStartsWithAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":30, + "l":[ + "u@", + "a@", + "b@" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"9e55f5cf" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"e170a185" + }, + "stringStartsWithAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":22, + "l":[ + "2_3a9dccacd7da15be19e54f7bca31fbe951fed6f6c2fab00fb8cae9f3316ca77f", + "2_6259e0c23524e7655dbf66e2b3aedc4186f26f594a700b3bc879928a226f5db7", + "2_734e111f23bd3a6fa57dfe234682cb70225d120911804b37ea03f25343db330f" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"1d9b7603" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"dd5b3211" + }, + "stringStartsWithDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":22, + "l":[ + "1_4e489426d4434d93128c8fceadbbb040d109874cb5d5fbe29fc8df5ad37284ea" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"3b409872" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"3659b0fe" + } + } + } + """.trimIndent() +} diff --git a/src/commonTest/kotlin/com/configcat/userattribute/data/StringConvertData.kt b/src/commonTest/kotlin/com/configcat/userattribute/data/StringConvertData.kt new file mode 100644 index 00000000..a6f7fcff --- /dev/null +++ b/src/commonTest/kotlin/com/configcat/userattribute/data/StringConvertData.kt @@ -0,0 +1,1412 @@ +package com.configcat.userattribute.data + +object StringConvertData : ConvertData { + override val sdkKey = "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ" + override val flagKey = "boolTextEqualsNumber" + override val defaultValue = false + override val remoteJson = """ + { + "p":{ + "u":"https://cdn-global.configcat.com", + "r":0, + "s":"brLK\u002BZy5OUFPomdNB48Sgv3HwDy6gXQ5q1\u002BvXlUxXws=" + }, + "f":{ + "allinone":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":20, + "s":"83ee2cd874eeb7ed1fbde0f16b8915a830f6c5a8e697ac8236c24c482f856045" + } + }, + { + "u":{ + "a":"Email", + "c":21, + "s":"83ee2cd874eeb7ed1fbde0f16b8915a830f6c5a8e697ac8236c24c482f856045" + } + } + ], + "s":{ + "v":{ + "s":"1h" + }, + "i":"e3a79156" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":28, + "s":"joe@example.com" + } + }, + { + "u":{ + "a":"Email", + "c":29, + "s":"joe@example.com" + } + } + ], + "s":{ + "v":{ + "s":"1c" + }, + "i":"ed60451a" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":16, + "l":[ + "83ee2cd874eeb7ed1fbde0f16b8915a830f6c5a8e697ac8236c24c482f856045" + ] + } + }, + { + "u":{ + "a":"Email", + "c":17, + "l":[ + "83ee2cd874eeb7ed1fbde0f16b8915a830f6c5a8e697ac8236c24c482f856045" + ] + } + } + ], + "s":{ + "v":{ + "s":"2h" + }, + "i":"aa24b7a3" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":0, + "l":[ + "joe@example.com" + ] + } + }, + { + "u":{ + "a":"Email", + "c":1, + "l":[ + "joe@example.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"2c" + }, + "i":"d37425a1" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":22, + "l":[ + "4_00903bfce5fabef65d1aad7f82d014b192b92ab90375b6a763e4e5cae119d698" + ] + } + }, + { + "u":{ + "a":"Email", + "c":23, + "l":[ + "4_00903bfce5fabef65d1aad7f82d014b192b92ab90375b6a763e4e5cae119d698" + ] + } + } + ], + "s":{ + "v":{ + "s":"3h" + }, + "i":"5e6e0c6c" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":30, + "l":[ + "joe@" + ] + } + }, + { + "u":{ + "a":"Email", + "c":31, + "l":[ + "joe@" + ] + } + } + ], + "s":{ + "v":{ + "s":"3c" + }, + "i":"5f562a70" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "12_8a189744250da86dcc8bf3af94f49ffcb26746d76eb9838bad2133e6f76c189f" + ] + } + }, + { + "u":{ + "a":"Email", + "c":25, + "l":[ + "12_8a189744250da86dcc8bf3af94f49ffcb26746d76eb9838bad2133e6f76c189f" + ] + } + } + ], + "s":{ + "v":{ + "s":"4h" + }, + "i":"91b91d69" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":32, + "l":[ + "@example.com" + ] + } + }, + { + "u":{ + "a":"Email", + "c":33, + "l":[ + "@example.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"4c" + }, + "i":"4c80a977" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "e@e" + ] + } + }, + { + "u":{ + "a":"Email", + "c":3, + "l":[ + "e@e" + ] + } + } + ], + "s":{ + "v":{ + "s":"5" + }, + "i":"dd12c429" + } + }, + { + "c":[ + { + "u":{ + "a":"Version", + "c":4, + "l":[ + "1.0.0" + ] + } + }, + { + "u":{ + "a":"Version", + "c":5, + "l":[ + "1.0.0" + ] + } + } + ], + "s":{ + "v":{ + "s":"6" + }, + "i":"dba5d266" + } + }, + { + "c":[ + { + "u":{ + "a":"Version", + "c":6, + "s":"1.0.1" + } + }, + { + "u":{ + "a":"Version", + "c":9, + "s":"1.0.1" + } + } + ], + "s":{ + "v":{ + "s":"7" + }, + "i":"1637ffc5" + } + }, + { + "c":[ + { + "u":{ + "a":"Version", + "c":8, + "s":"0.9.9" + } + }, + { + "u":{ + "a":"Version", + "c":7, + "s":"0.9.9" + } + } + ], + "s":{ + "v":{ + "s":"8" + }, + "i":"b084ddd6" + } + }, + { + "c":[ + { + "u":{ + "a":"Number", + "c":10, + "d":1 + } + }, + { + "u":{ + "a":"Number", + "c":11, + "d":1 + } + } + ], + "s":{ + "v":{ + "s":"9" + }, + "i":"d1d537a6" + } + }, + { + "c":[ + { + "u":{ + "a":"Number", + "c":12, + "d":1.1 + } + }, + { + "u":{ + "a":"Number", + "c":15, + "d":1.1 + } + } + ], + "s":{ + "v":{ + "s":"10" + }, + "i":"52c846d0" + } + }, + { + "c":[ + { + "u":{ + "a":"Number", + "c":14, + "d":0.9 + } + }, + { + "u":{ + "a":"Number", + "c":13, + "d":0.9 + } + } + ], + "s":{ + "v":{ + "s":"11" + }, + "i":"c91ffb7c" + } + }, + { + "c":[ + { + "u":{ + "a":"Date", + "c":18, + "d":1693497600 + } + }, + { + "u":{ + "a":"Date", + "c":19, + "d":1693497600 + } + } + ], + "s":{ + "v":{ + "s":"12" + }, + "i":"c12182ef" + } + }, + { + "c":[ + { + "u":{ + "a":"Country", + "c":26, + "l":[ + "fe9a56922634f9b8492bda6a11e4db55d2042b7474260d5d07c46df65114ab3c" + ] + } + }, + { + "u":{ + "a":"Country", + "c":27, + "l":[ + "fe9a56922634f9b8492bda6a11e4db55d2042b7474260d5d07c46df65114ab3c" + ] + } + } + ], + "s":{ + "v":{ + "s":"13h" + }, + "i":"a16b1a17" + } + }, + { + "c":[ + { + "u":{ + "a":"Country", + "c":34, + "l":[ + "USA" + ] + } + }, + { + "u":{ + "a":"Country", + "c":35, + "l":[ + "USA" + ] + } + } + ], + "s":{ + "v":{ + "s":"13c" + }, + "i":"1a17d1b3" + } + } + ], + "v":{ + "s":"default" + }, + "i":"9ff25f81" + }, + "arrayContainsCaseCheckDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":26, + "l":[ + "11bcd95e7760685f2cdca35f137d05fb70b98ed62a7126697df474ec43fd9d62" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"5d80eff1" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"ce055a38" + }, + "arrayContainsDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":26, + "l":[ + "05fa8abeef965b893e455dfef83db94ef28f01b0ea14b4c1f71929fc2f725753" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"147fdd01" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"5f573f9c" + }, + "arrayDoesNotContainCaseCheckDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":27, + "l":[ + "980707686b0e1854fa318924d3ff7ebf8f74412a3799c6087f3e59bf71279869" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"d4ad5730" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"df4915fd" + }, + "arrayDoesNotContainDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":27, + "l":[ + "8656713f187cf1d3492b092a5ffd2207930893b3ca365226af048763d5ed5283" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"c2161ac9" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"41910880" + }, + "boolTextEqualsNumber":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":28, + "s":"42" + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"28228808" + } + } + ], + "v":{ + "b":false + }, + "i":"fe23db9c" + }, + "boolTrueIn202304":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":19, + "d":1680307200 + } + }, + { + "u":{ + "a":"Custom1", + "c":18, + "d":1682899200 + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"6948d7cd" + } + } + ], + "v":{ + "b":false + }, + "i":"ae2a09bd" + }, + "countryPercentageAttribute":{ + "t":1, + "a":"Country", + "p":[ + { + "p":50, + "v":{ + "s":"Falcon" + }, + "i":"2b05fd81" + }, + { + "p":50, + "v":{ + "s":"Horse" + }, + "i":"e28b6a82" + } + ], + "v":{ + "s":"Chicken" + }, + "i":"29bb6bbb" + }, + "customPercentageAttribute":{ + "t":1, + "a":"Custom1", + "p":[ + { + "p":50, + "v":{ + "s":"Falcon" + }, + "i":"3715712d" + }, + { + "p":50, + "v":{ + "s":"Horse" + }, + "i":"7b3542d5" + } + ], + "v":{ + "s":"Chicken" + }, + "i":"50466fb6" + }, + "missingPercentageAttribute":{ + "t":1, + "a":"NotFound", + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "14_73c8547dd9486c7c73707d1fdea4abefdd67b5c99b699e8fba112515608a186e" + ] + } + } + ], + "p":[ + { + "p":50, + "v":{ + "s":"Falcon" + }, + "i":"4b7d88ba" + }, + { + "p":50, + "v":{ + "s":"Horse" + }, + "i":"a1c2c9a9" + } + ] + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "14_73c8547dd9486c7c73707d1fdea4abefdd67b5c99b699e8fba112515608a186e" + ] + } + } + ], + "s":{ + "v":{ + "s":"NotFound" + }, + "i":"8aa042fe" + } + } + ], + "v":{ + "s":"Chicken" + }, + "i":"e5107172" + }, + "stringArrayContainsAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":34, + "l":[ + "read", + "write", + "execute" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"9ddb8a37" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"0d45ab4b" + }, + "stringArrayContainsAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":26, + "l":[ + "5ac671434b10e94fce1e30b1a81466e3820adb6e61db3fd191a9b5e3a21dfa92", + "5db65a4bfc7a0ab53515015fc9fdd834034e7e1ce56dfed3e563e9e3a5d85ff6", + "872971fa051302ed22add5b89c772679dd825f3f57281fc4f4d06894d4c2962c" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"aa03b1ff" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"203317f5" + }, + "stringArrayNotContainsAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":35, + "l":[ + "read", + "write", + "execute" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"15c865df" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"6df210da" + }, + "stringArrayNotContainsAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":27, + "l":[ + "d0b563571357fa8c4e984428637569ecc7c3506671632770567711f8b90cb368", + "f9ebea55bb55e515339510ec020dcbfbe942a7e907cef9ce6d1bb6fdd735d8f1", + "d98bd0b50ab3def5463d5ef743063433bc13b328149aaa959dfc0b3afd042868" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"259816ba" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"60b961b0" + }, + "stringContainsAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@verizon.com", + "@verizon.net", + "@aol.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"09af657f" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"063bcf39" + }, + "stringDoseNotEqualDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":21, + "s":"abd8ae2ea7aeb1f19c08e4c71a53c7c9a12d189964b38a99c73b6fdd6a3f8097" + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"8e423808" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"1835a09a" + }, + "stringEndsWithAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":32, + "l":[ + "@verizon.com", + "@verizon.net", + "@aol.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"33d35402" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"31976ec3" + }, + "stringEndsWithAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "12_80b1d3bc456925165de90add33dc23e2561c4e4cde5f52bd47fcfaeadd915eab", + "12_beb9d6142d7a6f3c72f52d79769022c1cf27610b6208a86712a25cd36dc2778c", + "8_bf64c8d66c84e43e226c2b8f59cf74a9616acddddb18d9cbd8ecc12711df52a8" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"7231ddf8" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"de17fd2a" + }, + "stringEndsWithDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "14_8c1ebc832cfbeedf2b18e9c3041248164f9c60be1f3374d3884604f2b20d8aa3" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"d7a00741" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"45b7d922" + }, + "stringEqualsCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":28, + "s":"a@configcat.com" + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"087e01dd" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"89785ab3" + }, + "stringEqualsDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":20, + "s":"3fd1f9f6da935590c3e5fce1e8c1182a80f7ad649db89a546651c4d505ef8522" + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"703c31ed" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"adc0b01c" + }, + "stringNotContainsAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":3, + "l":[ + "@verizon.com", + "@verizon.net", + "@aol.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"49627b36" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"36848b03" + }, + "stringNotEndsWithAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":33, + "l":[ + "@verizon.com", + "@verizon.net", + "@aol.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"886ced9d" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"864b6202" + }, + "stringNotEndsWithAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":25, + "l":[ + "12_fb5e2ce7ba0191194782c183ab7b63995790f6ebf148b8fbb94407f656cc9126", + "12_58bb59d19771d0197cdcd993c516df9564dcfb31a0e61fa4d43c91dc0c7c7729", + "8_e6bf34ddc03127d516fa6193168276ce3bfcb9c964786b52ce753ba5eb4e6c65" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"6eb0ec3a" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"7020bcd6" + }, + "stringNotEndsWithDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":25, + "l":[ + "14_9d758cb8cadc3493f7f56e505e81e28cd26c4fa0b0913b1db205f27855345d00" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"d37b6f18" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"91ba1bcb" + }, + "stringNotEqualsCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":29, + "s":"b@configcat.com" + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"3ace20fb" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"09c9725f" + }, + "stringNotStartsWithAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":31, + "l":[ + "u@", + "a@", + "b@" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"3717f2ca" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"1d661433" + }, + "stringNotStartsWithAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":23, + "l":[ + "2_2c317623f35bffec808ccaed2a3f29c7a78ab2276d6821ae17e7e22b4c7ecb1d", + "2_3f1f139ea0136aee360ae7c35a3e9450ce6796c8e1b0855c27436148ed345dc2", + "2_80d7473697ee1d8259959951fe17947cfda43c5f3cee8c17f439c04bab5606ef" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"b5ba025e" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"c35929e3" + }, + "stringNotStartsWithDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":23, + "l":[ + "1_4708693c9a610590f6e487a659a6f7e7fb6509ed2c158a5299fdef585115dd55" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"72c4e1ac" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"2b16da78" + }, + "stringStartsWithAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":30, + "l":[ + "u@", + "a@", + "b@" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"9e55f5cf" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"e170a185" + }, + "stringStartsWithAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":22, + "l":[ + "2_3a9dccacd7da15be19e54f7bca31fbe951fed6f6c2fab00fb8cae9f3316ca77f", + "2_6259e0c23524e7655dbf66e2b3aedc4186f26f594a700b3bc879928a226f5db7", + "2_734e111f23bd3a6fa57dfe234682cb70225d120911804b37ea03f25343db330f" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"1d9b7603" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"dd5b3211" + }, + "stringStartsWithDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":22, + "l":[ + "1_4e489426d4434d93128c8fceadbbb040d109874cb5d5fbe29fc8df5ad37284ea" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"3b409872" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"3659b0fe" + } + } + } + """.trimIndent() +} diff --git a/src/darwinMain/kotlin/com/configcat/NumberFormatter.kt b/src/darwinMain/kotlin/com/configcat/NumberFormatter.kt new file mode 100644 index 00000000..072dd068 --- /dev/null +++ b/src/darwinMain/kotlin/com/configcat/NumberFormatter.kt @@ -0,0 +1,41 @@ +package com.configcat + +import kotlinx.cinterop.UnsafeNumber +import platform.Foundation.NSNumber +import platform.Foundation.NSNumberFormatter +import kotlin.math.abs + +@OptIn(UnsafeNumber::class) +internal actual fun doubleToString(doubleToString: Double): String { + // Handle Double.NaN, Double.POSITIVE_INFINITY and Double.NEGATIVE_INFINITY + if (doubleToString.isNaN() || doubleToString.isInfinite()) { + return doubleToString.toString() + } + + // To get similar result between different SDKs the Double value format is modified. + // Between 1e-6 and 1e21 we don't use scientific-notation. Over these limits scientific-notation used but the + // ExponentSeparator replaced with "e" and "e+". + // "." used as decimal separator in all cases. + val abs = abs(doubleToString) + val formatter = NSNumberFormatter() + formatter.usesGroupingSeparator = false + formatter.minimumFractionDigits = 0u + formatter.maximumFractionDigits = 17u + if (1e-6 <= abs && abs < 1e21) { + formatter.numberStyle = 1u + return formatter.stringFromNumber(NSNumber(doubleToString)) ?: "" + } else { + val str = NSNumber(doubleToString) + return str.description ?: "" + } +} + +@OptIn(UnsafeNumber::class) +internal actual fun formatDoubleForLog(doubleToFormat: Double): String { + val formatter = NSNumberFormatter() + formatter.usesGroupingSeparator = false + formatter.minimumFractionDigits = 0u + formatter.maximumFractionDigits = 4u + formatter.numberStyle = 1u + return formatter.stringFromNumber(NSNumber(doubleToFormat)) ?: "" +} diff --git a/src/darwinMain/kotlin/com/configcat/fetch/HttpRequestBuilder.kt b/src/darwinMain/kotlin/com/configcat/fetch/HttpRequestBuilder.kt new file mode 100644 index 00000000..162c1103 --- /dev/null +++ b/src/darwinMain/kotlin/com/configcat/fetch/HttpRequestBuilder.kt @@ -0,0 +1,10 @@ +package com.configcat.fetch + +import io.ktor.client.request.* + +internal actual fun httpRequestBuilder( + configCatUserAgent: String, + eTag: String +): HttpRequestBuilder { + return commonHttpRequestBuilder(configCatUserAgent, eTag) +} diff --git a/src/darwinMain/kotlin/com/configcat/override/SettingConverter.kt b/src/darwinMain/kotlin/com/configcat/override/SettingConverter.kt new file mode 100644 index 00000000..607baf5c --- /dev/null +++ b/src/darwinMain/kotlin/com/configcat/override/SettingConverter.kt @@ -0,0 +1,7 @@ +package com.configcat.override + +import com.configcat.model.Setting + +internal actual fun convertToSetting(value: Any): Setting { + return commonConvertToSetting(value) +} diff --git a/src/darwinTest/kotlin/com/configcat/DarwinEvaluationTests.kt b/src/darwinTest/kotlin/com/configcat/DarwinEvaluationTests.kt new file mode 100644 index 00000000..b8fde44e --- /dev/null +++ b/src/darwinTest/kotlin/com/configcat/DarwinEvaluationTests.kt @@ -0,0 +1,96 @@ + +import com.configcat.ConfigCatClient +import com.configcat.SingleValueCache +import com.configcat.data.DarwinComparatorsTests +import com.configcat.data.DarwinEpochDateValidationTests +import com.configcat.evaluation.EvaluationTestLogger +import com.configcat.evaluation.data.* +import com.configcat.log.LogLevel +import com.configcat.manualPoll +import io.ktor.client.engine.mock.* +import io.ktor.http.* +import io.ktor.util.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.fail + +@OptIn(ExperimentalCoroutinesApi::class) +class DarwinEvaluationTests { + + @Test + fun testComparators() = runTest { + // The test contains formatted double value, which is different in case of JS module + testEvaluation(DarwinComparatorsTests) + } + + @Test + fun testEpochDateValidation() = runTest { + // The test contains formatted double value, which is different in case of JS module + testEvaluation(DarwinEpochDateValidationTests) + } + + private suspend fun testEvaluation(testSet: TestSet) { + var sdkKey = testSet.sdkKey + if (sdkKey.isNullOrEmpty()) { + sdkKey = TEST_SDK_KEY + } + + val mockEngine = MockEngine { + respond( + content = testSet.jsonOverride, + status = HttpStatusCode.OK, + headersOf(Pair("ETag", listOf("fakeETag"))) + ) + } + + val evaluationTestLogger = EvaluationTestLogger() + val client = ConfigCatClient(sdkKey) { + pollingMode = manualPoll() + baseUrl = testSet.baseUrl + httpEngine = mockEngine + logger = evaluationTestLogger + logLevel = LogLevel.INFO + + // add empty SingleValueCache to avoid JS extra cache logs + configCache = SingleValueCache("") + } + client.forceRefresh() + + val tests = testSet.tests + val errors: ArrayList = arrayListOf() + for (test in tests!!) { + val settingKey = test.key + + val result: Any? = client.getAnyValue(settingKey, test.defaultValue, test.user) + if (test.returnValue != result) { + errors.add("Return value mismatch for test: %s Test Key: $settingKey Expected: ${test.returnValue}, Result: $result \n") + } + val expectedLog = test.expectedLog + val logResultBuilder = StringBuilder() + val logsList = evaluationTestLogger.getLogList() + for (i in logsList.indices) { + val log = logsList[i] + logResultBuilder.append(log.logMessage) + if (i != logsList.size - 1) { + logResultBuilder.append("\n") + } + } + val logResult: String = logResultBuilder.toString() + if (expectedLog != logResult) { + errors.add("Log mismatch for test: %s Test Key: $settingKey Expected:\n$expectedLog\nResult:\n$logResult\n") + } + evaluationTestLogger.resetLogList() + } + + client.close() + + if (errors.isNotEmpty()) { + fail(errors.joinToString("\n")) + } + } + + companion object { + private const val TEST_SDK_KEY = "configcat-sdk-test-key/0000000000000000000000" + } +} diff --git a/src/darwinTest/kotlin/com/configcat/data/DarwinComparatorsTests.kt b/src/darwinTest/kotlin/com/configcat/data/DarwinComparatorsTests.kt new file mode 100644 index 00000000..c3beb2bf --- /dev/null +++ b/src/darwinTest/kotlin/com/configcat/data/DarwinComparatorsTests.kt @@ -0,0 +1,1459 @@ +package com.configcat.data + +import com.configcat.ConfigCatUser +import com.configcat.evaluation.data.TestCase +import com.configcat.evaluation.data.TestSet + +object DarwinComparatorsTests : TestSet { + override val sdkKey = "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ" + override val baseUrl = null + override val jsonOverride = """ + { + "p":{ + "u":"https://cdn-global.configcat.com", + "r":0, + "s":"JEl\u002BhoGfr/01JCnpxr7kOCIoB2bYAM3uTMShm6HiAc4=" + }, + "f":{ + "allinone":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":20, + "s":"0740e9103e26acb85b086dd9a15eaa84246902f096655371a5f001b9e13754e8" + } + }, + { + "u":{ + "a":"Email", + "c":21, + "s":"0740e9103e26acb85b086dd9a15eaa84246902f096655371a5f001b9e13754e8" + } + } + ], + "s":{ + "v":{ + "s":"1h" + }, + "i":"e3a79156" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":28, + "s":"joe@example.com" + } + }, + { + "u":{ + "a":"Email", + "c":29, + "s":"joe@example.com" + } + } + ], + "s":{ + "v":{ + "s":"1c" + }, + "i":"ed60451a" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":16, + "l":[ + "0740e9103e26acb85b086dd9a15eaa84246902f096655371a5f001b9e13754e8" + ] + } + }, + { + "u":{ + "a":"Email", + "c":17, + "l":[ + "0740e9103e26acb85b086dd9a15eaa84246902f096655371a5f001b9e13754e8" + ] + } + } + ], + "s":{ + "v":{ + "s":"2h" + }, + "i":"aa24b7a3" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":0, + "l":[ + "joe@example.com" + ] + } + }, + { + "u":{ + "a":"Email", + "c":1, + "l":[ + "joe@example.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"2c" + }, + "i":"d37425a1" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":22, + "l":[ + "4_e99c716658ca0b1035394161a3ca54f8dc688930ad90bed26aeff075cb947397" + ] + } + }, + { + "u":{ + "a":"Email", + "c":23, + "l":[ + "4_e99c716658ca0b1035394161a3ca54f8dc688930ad90bed26aeff075cb947397" + ] + } + } + ], + "s":{ + "v":{ + "s":"3h" + }, + "i":"5e6e0c6c" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":30, + "l":[ + "joe@" + ] + } + }, + { + "u":{ + "a":"Email", + "c":31, + "l":[ + "joe@" + ] + } + } + ], + "s":{ + "v":{ + "s":"3c" + }, + "i":"5f562a70" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "12_29030906a5c2729247ccad10154b56b84d61ee4d732361e0ba7c3817da4f91b3" + ] + } + }, + { + "u":{ + "a":"Email", + "c":25, + "l":[ + "12_29030906a5c2729247ccad10154b56b84d61ee4d732361e0ba7c3817da4f91b3" + ] + } + } + ], + "s":{ + "v":{ + "s":"4h" + }, + "i":"91b91d69" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":32, + "l":[ + "@example.com" + ] + } + }, + { + "u":{ + "a":"Email", + "c":33, + "l":[ + "@example.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"4c" + }, + "i":"4c80a977" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "e@e" + ] + } + }, + { + "u":{ + "a":"Email", + "c":3, + "l":[ + "e@e" + ] + } + } + ], + "s":{ + "v":{ + "s":"5" + }, + "i":"dd12c429" + } + }, + { + "c":[ + { + "u":{ + "a":"Version", + "c":4, + "l":[ + "1.0.0" + ] + } + }, + { + "u":{ + "a":"Version", + "c":5, + "l":[ + "1.0.0" + ] + } + } + ], + "s":{ + "v":{ + "s":"6" + }, + "i":"dba5d266" + } + }, + { + "c":[ + { + "u":{ + "a":"Version", + "c":6, + "s":"1.0.1" + } + }, + { + "u":{ + "a":"Version", + "c":9, + "s":"1.0.1" + } + } + ], + "s":{ + "v":{ + "s":"7" + }, + "i":"1637ffc5" + } + }, + { + "c":[ + { + "u":{ + "a":"Version", + "c":8, + "s":"0.9.9" + } + }, + { + "u":{ + "a":"Version", + "c":7, + "s":"0.9.9" + } + } + ], + "s":{ + "v":{ + "s":"8" + }, + "i":"b084ddd6" + } + }, + { + "c":[ + { + "u":{ + "a":"Number", + "c":10, + "d":1 + } + }, + { + "u":{ + "a":"Number", + "c":11, + "d":1 + } + } + ], + "s":{ + "v":{ + "s":"9" + }, + "i":"d1d537a6" + } + }, + { + "c":[ + { + "u":{ + "a":"Number", + "c":12, + "d":1.1 + } + }, + { + "u":{ + "a":"Number", + "c":15, + "d":1.1 + } + } + ], + "s":{ + "v":{ + "s":"10" + }, + "i":"52c846d0" + } + }, + { + "c":[ + { + "u":{ + "a":"Number", + "c":14, + "d":0.9 + } + }, + { + "u":{ + "a":"Number", + "c":13, + "d":0.9 + } + } + ], + "s":{ + "v":{ + "s":"11" + }, + "i":"c91ffb7c" + } + }, + { + "c":[ + { + "u":{ + "a":"Date", + "c":18, + "d":1693497600 + } + }, + { + "u":{ + "a":"Date", + "c":19, + "d":1693497600 + } + } + ], + "s":{ + "v":{ + "s":"12" + }, + "i":"c12182ef" + } + }, + { + "c":[ + { + "u":{ + "a":"Country", + "c":26, + "l":[ + "5a85699e7343a36d89ee75dca859f7a73cb6be89182095bffb021d1d78de046c" + ] + } + }, + { + "u":{ + "a":"Country", + "c":27, + "l":[ + "5a85699e7343a36d89ee75dca859f7a73cb6be89182095bffb021d1d78de046c" + ] + } + } + ], + "s":{ + "v":{ + "s":"13h" + }, + "i":"a16b1a17" + } + }, + { + "c":[ + { + "u":{ + "a":"Country", + "c":34, + "l":[ + "USA" + ] + } + }, + { + "u":{ + "a":"Country", + "c":35, + "l":[ + "USA" + ] + } + } + ], + "s":{ + "v":{ + "s":"13c" + }, + "i":"1a17d1b3" + } + } + ], + "v":{ + "s":"default" + }, + "i":"9ff25f81" + }, + "arrayContainsCaseCheckDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":26, + "l":[ + "00083f86e0f648b23f6721d43033bbef14378266fbf7de8a6760cc2ad237e9f3" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"5d80eff1" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"ce055a38" + }, + "arrayContainsDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":26, + "l":[ + "c2d5024661bc0e13f769cbb28bbfab7b78dac88b7876f007020f2e7cd47b1114" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"147fdd01" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"5f573f9c" + }, + "arrayDoesNotContainCaseCheckDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":27, + "l":[ + "14748140c64ecab48c7fd13f03811fe6390c8f578c99df96cf36fc2c6152f660" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"d4ad5730" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"df4915fd" + }, + "arrayDoesNotContainDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":27, + "l":[ + "bc0c24462c5098e434c63c7fcc6343af5000a4e7affd309f365edd4ccb7f428b" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"c2161ac9" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"41910880" + }, + "boolTrueIn202304":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":19, + "d":1680307200 + } + }, + { + "u":{ + "a":"Custom1", + "c":18, + "d":1682899200 + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"6948d7cd" + } + } + ], + "v":{ + "b":false + }, + "i":"ae2a09bd" + }, + "countryPercentageAttribute":{ + "t":1, + "a":"Country", + "p":[ + { + "p":50, + "v":{ + "s":"Falcon" + }, + "i":"2b05fd81" + }, + { + "p":50, + "v":{ + "s":"Horse" + }, + "i":"e28b6a82" + } + ], + "v":{ + "s":"Chicken" + }, + "i":"29bb6bbb" + }, + "customPercentageAttribute":{ + "t":1, + "a":"Custom1", + "p":[ + { + "p":50, + "v":{ + "s":"Falcon" + }, + "i":"3715712d" + }, + { + "p":50, + "v":{ + "s":"Horse" + }, + "i":"7b3542d5" + } + ], + "v":{ + "s":"Chicken" + }, + "i":"50466fb6" + }, + "missingPercentageAttribute":{ + "t":1, + "a":"NotFound", + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "14_902a42101e8b77851c98456b383fd959ed0f5aed5b919b4a623c8c756cf0c3ab" + ] + } + } + ], + "p":[ + { + "p":50, + "v":{ + "s":"Falcon" + }, + "i":"4b7d88ba" + }, + { + "p":50, + "v":{ + "s":"Horse" + }, + "i":"a1c2c9a9" + } + ] + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "14_902a42101e8b77851c98456b383fd959ed0f5aed5b919b4a623c8c756cf0c3ab" + ] + } + } + ], + "s":{ + "v":{ + "s":"NotFound" + }, + "i":"8aa042fe" + } + } + ], + "v":{ + "s":"Chicken" + }, + "i":"e5107172" + }, + "stringArrayContainsAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":34, + "l":[ + "read", + "write", + "execute" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"9ddb8a37" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"0d45ab4b" + }, + "stringArrayContainsAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":26, + "l":[ + "090415ce6b462a2152e06d68aeb7c452a564d19a262eb959c510636a189e105d", + "0aa50b49ca02a59cf507856d88ab25b76a7e69e553d25e67254359d8bbb8b1d6", + "1aa91982f7ccae1943f05ac437d635b3261e3ce06aba846dd2ecc6332e4675c2" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"aa03b1ff" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"203317f5" + }, + "stringArrayNotContainsAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":35, + "l":[ + "read", + "write", + "execute" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"15c865df" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"6df210da" + }, + "stringArrayNotContainsAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":27, + "l":[ + "b645a36d4d2e24a0612296ded074eace87ce10a0da21c5cb2ac9a6dccbe79cc5", + "552a92975423ea255384f48a6387be172c05ac72238f90050cdfe27cc4659cfd", + "4c00d4171202dbeb35830b1df8e47185968351c771bb2f633ea50ac1c049e016" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"259816ba" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"60b961b0" + }, + "stringContainsAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@verizon.com", + "@verizon.net", + "@aol.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"09af657f" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"063bcf39" + }, + "stringDoseNotEqualDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":21, + "s":"3cb496a1c3844215c784116ce9e91c143860d7ef18f7fadadcc901b8df5d235c" + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"8e423808" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"1835a09a" + }, + "stringEndsWithAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":32, + "l":[ + "@verizon.com", + "@verizon.net", + "@aol.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"33d35402" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"31976ec3" + }, + "stringEndsWithAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "12_781f5f9b054a14721a835fcdd2d03ac6d45d99eb55b387bf5938904c8f65aa35", + "12_d15f199cfbd96ef1c6f7683e66d5e0f85c9c591ce377289abb2e785fc71bbdf1", + "8_6ee234c513b7518bd705cc1049576c1e757a201200ff26a6a3a91821f954c6cb" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"7231ddf8" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"de17fd2a" + }, + "stringEndsWithDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "14_236ec291c9b54fad3373a0b7e0e465b33459198fd1027838c101516ad8ae1b39" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"d7a00741" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"45b7d922" + }, + "stringEqualsCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":28, + "s":"a@configcat.com" + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"087e01dd" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"89785ab3" + }, + "stringEqualsDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":20, + "s":"86e6b430939acac093f3d8c48be10798896f0abf3b96e2e39080acedd925d887" + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"703c31ed" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"adc0b01c" + }, + "stringNotContainsAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":3, + "l":[ + "@verizon.com", + "@verizon.net", + "@aol.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"49627b36" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"36848b03" + }, + "stringNotEndsWithAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":33, + "l":[ + "@verizon.com", + "@verizon.net", + "@aol.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"886ced9d" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"864b6202" + }, + "stringNotEndsWithAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":25, + "l":[ + "12_295e66dc483034349bc6cffd14190ccff949ac1f34df5fbf01437b8162719b98", + "12_11933417446186b92f63db5931a275f156485d0aef5ee8e265afb350921bd0b1", + "8_aa181c1d9462e3916cb05669e2c408541edf03dad19a9655b340eb32ddd4d060" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"6eb0ec3a" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"7020bcd6" + }, + "stringNotEndsWithDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":25, + "l":[ + "14_141d3f398885dd06d6c14e6629602125d8cb0dddf2ef85896f682c74abb4bc28" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"d37b6f18" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"91ba1bcb" + }, + "stringNotEqualsCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":29, + "s":"b@configcat.com" + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"3ace20fb" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"09c9725f" + }, + "stringNotStartsWithAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":31, + "l":[ + "u@", + "a@", + "b@" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"3717f2ca" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"1d661433" + }, + "stringNotStartsWithAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":23, + "l":[ + "2_b0ab1063bba4431f95710a8bd9c9e2bcd1cebc09fe6803cf87ee501d045e8ee0", + "2_9206a0a6b987410488c8020650788c597509af05aa5327f92cae1c999c401c34", + "2_d8f5c200e2c54b0f84f7df024576f799f484d0e258c6058142a4993daa5e2998" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"b5ba025e" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"c35929e3" + }, + "stringNotStartsWithDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":23, + "l":[ + "1_061a38ed8d5955d90b88d1b1949512a45032390a2581f3c7e36597f7459a48c7" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"72c4e1ac" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"2b16da78" + }, + "stringStartsWithAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":30, + "l":[ + "u@", + "a@", + "b@" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"9e55f5cf" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"e170a185" + }, + "stringStartsWithAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":22, + "l":[ + "2_52f47d1a193071a304e41812055cc1282d90287e5c993ae159fa08fb1ccd3656", + "2_6e5ae1bb586660088d330ac106bbf6b7f2b43be1ca70dbb766f203b76bc84049", + "2_3edba8b738135ad32e85d8695acb8d8511ac67d83f50da55abd9ba469da88efe" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"1d9b7603" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"dd5b3211" + }, + "stringStartsWithDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":22, + "l":[ + "1_55e0b0566d89dc0fda2323efcfb958c782a4648513bdcc4dc84b044fd34230dd" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"3b409872" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"3659b0fe" + } + } +} + """.trimIndent() + override val tests: Array = arrayOf( + TestCase( + key = "allinone", + defaultValue = "", + returnValue = "default", + user = ConfigCatUser( + "12345", + "joe@example.com", + "[\"USA\"]", + custom = mapOf("Version" to "1.0.0", "Number" to "1.0", "Date" to "1693497500") + ), + expectedLog = """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/src/darwinTest/kotlin/com/configcat/data/DarwinEpochDateValidationTests.kt b/src/darwinTest/kotlin/com/configcat/data/DarwinEpochDateValidationTests.kt new file mode 100644 index 00000000..815311a4 --- /dev/null +++ b/src/darwinTest/kotlin/com/configcat/data/DarwinEpochDateValidationTests.kt @@ -0,0 +1,1404 @@ +package com.configcat.data + +import com.configcat.ConfigCatUser +import com.configcat.evaluation.data.TestCase +import com.configcat.evaluation.data.TestSet + +object DarwinEpochDateValidationTests : TestSet { + override val sdkKey = "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ" + override val baseUrl = null + override val jsonOverride = """ + { + "p":{ + "u":"https://cdn-global.configcat.com", + "r":0, + "s":"JEl\u002BhoGfr/01JCnpxr7kOCIoB2bYAM3uTMShm6HiAc4=" + }, + "f":{ + "allinone":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":20, + "s":"0740e9103e26acb85b086dd9a15eaa84246902f096655371a5f001b9e13754e8" + } + }, + { + "u":{ + "a":"Email", + "c":21, + "s":"0740e9103e26acb85b086dd9a15eaa84246902f096655371a5f001b9e13754e8" + } + } + ], + "s":{ + "v":{ + "s":"1h" + }, + "i":"e3a79156" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":28, + "s":"joe@example.com" + } + }, + { + "u":{ + "a":"Email", + "c":29, + "s":"joe@example.com" + } + } + ], + "s":{ + "v":{ + "s":"1c" + }, + "i":"ed60451a" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":16, + "l":[ + "0740e9103e26acb85b086dd9a15eaa84246902f096655371a5f001b9e13754e8" + ] + } + }, + { + "u":{ + "a":"Email", + "c":17, + "l":[ + "0740e9103e26acb85b086dd9a15eaa84246902f096655371a5f001b9e13754e8" + ] + } + } + ], + "s":{ + "v":{ + "s":"2h" + }, + "i":"aa24b7a3" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":0, + "l":[ + "joe@example.com" + ] + } + }, + { + "u":{ + "a":"Email", + "c":1, + "l":[ + "joe@example.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"2c" + }, + "i":"d37425a1" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":22, + "l":[ + "4_e99c716658ca0b1035394161a3ca54f8dc688930ad90bed26aeff075cb947397" + ] + } + }, + { + "u":{ + "a":"Email", + "c":23, + "l":[ + "4_e99c716658ca0b1035394161a3ca54f8dc688930ad90bed26aeff075cb947397" + ] + } + } + ], + "s":{ + "v":{ + "s":"3h" + }, + "i":"5e6e0c6c" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":30, + "l":[ + "joe@" + ] + } + }, + { + "u":{ + "a":"Email", + "c":31, + "l":[ + "joe@" + ] + } + } + ], + "s":{ + "v":{ + "s":"3c" + }, + "i":"5f562a70" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "12_29030906a5c2729247ccad10154b56b84d61ee4d732361e0ba7c3817da4f91b3" + ] + } + }, + { + "u":{ + "a":"Email", + "c":25, + "l":[ + "12_29030906a5c2729247ccad10154b56b84d61ee4d732361e0ba7c3817da4f91b3" + ] + } + } + ], + "s":{ + "v":{ + "s":"4h" + }, + "i":"91b91d69" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":32, + "l":[ + "@example.com" + ] + } + }, + { + "u":{ + "a":"Email", + "c":33, + "l":[ + "@example.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"4c" + }, + "i":"4c80a977" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "e@e" + ] + } + }, + { + "u":{ + "a":"Email", + "c":3, + "l":[ + "e@e" + ] + } + } + ], + "s":{ + "v":{ + "s":"5" + }, + "i":"dd12c429" + } + }, + { + "c":[ + { + "u":{ + "a":"Version", + "c":4, + "l":[ + "1.0.0" + ] + } + }, + { + "u":{ + "a":"Version", + "c":5, + "l":[ + "1.0.0" + ] + } + } + ], + "s":{ + "v":{ + "s":"6" + }, + "i":"dba5d266" + } + }, + { + "c":[ + { + "u":{ + "a":"Version", + "c":6, + "s":"1.0.1" + } + }, + { + "u":{ + "a":"Version", + "c":9, + "s":"1.0.1" + } + } + ], + "s":{ + "v":{ + "s":"7" + }, + "i":"1637ffc5" + } + }, + { + "c":[ + { + "u":{ + "a":"Version", + "c":8, + "s":"0.9.9" + } + }, + { + "u":{ + "a":"Version", + "c":7, + "s":"0.9.9" + } + } + ], + "s":{ + "v":{ + "s":"8" + }, + "i":"b084ddd6" + } + }, + { + "c":[ + { + "u":{ + "a":"Number", + "c":10, + "d":1 + } + }, + { + "u":{ + "a":"Number", + "c":11, + "d":1 + } + } + ], + "s":{ + "v":{ + "s":"9" + }, + "i":"d1d537a6" + } + }, + { + "c":[ + { + "u":{ + "a":"Number", + "c":12, + "d":1.1 + } + }, + { + "u":{ + "a":"Number", + "c":15, + "d":1.1 + } + } + ], + "s":{ + "v":{ + "s":"10" + }, + "i":"52c846d0" + } + }, + { + "c":[ + { + "u":{ + "a":"Number", + "c":14, + "d":0.9 + } + }, + { + "u":{ + "a":"Number", + "c":13, + "d":0.9 + } + } + ], + "s":{ + "v":{ + "s":"11" + }, + "i":"c91ffb7c" + } + }, + { + "c":[ + { + "u":{ + "a":"Date", + "c":18, + "d":1693497600 + } + }, + { + "u":{ + "a":"Date", + "c":19, + "d":1693497600 + } + } + ], + "s":{ + "v":{ + "s":"12" + }, + "i":"c12182ef" + } + }, + { + "c":[ + { + "u":{ + "a":"Country", + "c":26, + "l":[ + "5a85699e7343a36d89ee75dca859f7a73cb6be89182095bffb021d1d78de046c" + ] + } + }, + { + "u":{ + "a":"Country", + "c":27, + "l":[ + "5a85699e7343a36d89ee75dca859f7a73cb6be89182095bffb021d1d78de046c" + ] + } + } + ], + "s":{ + "v":{ + "s":"13h" + }, + "i":"a16b1a17" + } + }, + { + "c":[ + { + "u":{ + "a":"Country", + "c":34, + "l":[ + "USA" + ] + } + }, + { + "u":{ + "a":"Country", + "c":35, + "l":[ + "USA" + ] + } + } + ], + "s":{ + "v":{ + "s":"13c" + }, + "i":"1a17d1b3" + } + } + ], + "v":{ + "s":"default" + }, + "i":"9ff25f81" + }, + "arrayContainsCaseCheckDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":26, + "l":[ + "00083f86e0f648b23f6721d43033bbef14378266fbf7de8a6760cc2ad237e9f3" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"5d80eff1" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"ce055a38" + }, + "arrayContainsDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":26, + "l":[ + "c2d5024661bc0e13f769cbb28bbfab7b78dac88b7876f007020f2e7cd47b1114" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"147fdd01" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"5f573f9c" + }, + "arrayDoesNotContainCaseCheckDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":27, + "l":[ + "14748140c64ecab48c7fd13f03811fe6390c8f578c99df96cf36fc2c6152f660" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"d4ad5730" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"df4915fd" + }, + "arrayDoesNotContainDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":27, + "l":[ + "bc0c24462c5098e434c63c7fcc6343af5000a4e7affd309f365edd4ccb7f428b" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"c2161ac9" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"41910880" + }, + "boolTrueIn202304":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":19, + "d":1680307200 + } + }, + { + "u":{ + "a":"Custom1", + "c":18, + "d":1682899200 + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"6948d7cd" + } + } + ], + "v":{ + "b":false + }, + "i":"ae2a09bd" + }, + "countryPercentageAttribute":{ + "t":1, + "a":"Country", + "p":[ + { + "p":50, + "v":{ + "s":"Falcon" + }, + "i":"2b05fd81" + }, + { + "p":50, + "v":{ + "s":"Horse" + }, + "i":"e28b6a82" + } + ], + "v":{ + "s":"Chicken" + }, + "i":"29bb6bbb" + }, + "customPercentageAttribute":{ + "t":1, + "a":"Custom1", + "p":[ + { + "p":50, + "v":{ + "s":"Falcon" + }, + "i":"3715712d" + }, + { + "p":50, + "v":{ + "s":"Horse" + }, + "i":"7b3542d5" + } + ], + "v":{ + "s":"Chicken" + }, + "i":"50466fb6" + }, + "missingPercentageAttribute":{ + "t":1, + "a":"NotFound", + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "14_902a42101e8b77851c98456b383fd959ed0f5aed5b919b4a623c8c756cf0c3ab" + ] + } + } + ], + "p":[ + { + "p":50, + "v":{ + "s":"Falcon" + }, + "i":"4b7d88ba" + }, + { + "p":50, + "v":{ + "s":"Horse" + }, + "i":"a1c2c9a9" + } + ] + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "14_902a42101e8b77851c98456b383fd959ed0f5aed5b919b4a623c8c756cf0c3ab" + ] + } + } + ], + "s":{ + "v":{ + "s":"NotFound" + }, + "i":"8aa042fe" + } + } + ], + "v":{ + "s":"Chicken" + }, + "i":"e5107172" + }, + "stringArrayContainsAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":34, + "l":[ + "read", + "write", + "execute" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"9ddb8a37" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"0d45ab4b" + }, + "stringArrayContainsAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":26, + "l":[ + "090415ce6b462a2152e06d68aeb7c452a564d19a262eb959c510636a189e105d", + "0aa50b49ca02a59cf507856d88ab25b76a7e69e553d25e67254359d8bbb8b1d6", + "1aa91982f7ccae1943f05ac437d635b3261e3ce06aba846dd2ecc6332e4675c2" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"aa03b1ff" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"203317f5" + }, + "stringArrayNotContainsAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":35, + "l":[ + "read", + "write", + "execute" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"15c865df" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"6df210da" + }, + "stringArrayNotContainsAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":27, + "l":[ + "b645a36d4d2e24a0612296ded074eace87ce10a0da21c5cb2ac9a6dccbe79cc5", + "552a92975423ea255384f48a6387be172c05ac72238f90050cdfe27cc4659cfd", + "4c00d4171202dbeb35830b1df8e47185968351c771bb2f633ea50ac1c049e016" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"259816ba" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"60b961b0" + }, + "stringContainsAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@verizon.com", + "@verizon.net", + "@aol.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"09af657f" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"063bcf39" + }, + "stringDoseNotEqualDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":21, + "s":"3cb496a1c3844215c784116ce9e91c143860d7ef18f7fadadcc901b8df5d235c" + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"8e423808" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"1835a09a" + }, + "stringEndsWithAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":32, + "l":[ + "@verizon.com", + "@verizon.net", + "@aol.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"33d35402" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"31976ec3" + }, + "stringEndsWithAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "12_781f5f9b054a14721a835fcdd2d03ac6d45d99eb55b387bf5938904c8f65aa35", + "12_d15f199cfbd96ef1c6f7683e66d5e0f85c9c591ce377289abb2e785fc71bbdf1", + "8_6ee234c513b7518bd705cc1049576c1e757a201200ff26a6a3a91821f954c6cb" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"7231ddf8" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"de17fd2a" + }, + "stringEndsWithDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "14_236ec291c9b54fad3373a0b7e0e465b33459198fd1027838c101516ad8ae1b39" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"d7a00741" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"45b7d922" + }, + "stringEqualsCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":28, + "s":"a@configcat.com" + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"087e01dd" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"89785ab3" + }, + "stringEqualsDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":20, + "s":"86e6b430939acac093f3d8c48be10798896f0abf3b96e2e39080acedd925d887" + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"703c31ed" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"adc0b01c" + }, + "stringNotContainsAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":3, + "l":[ + "@verizon.com", + "@verizon.net", + "@aol.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"49627b36" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"36848b03" + }, + "stringNotEndsWithAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":33, + "l":[ + "@verizon.com", + "@verizon.net", + "@aol.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"886ced9d" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"864b6202" + }, + "stringNotEndsWithAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":25, + "l":[ + "12_295e66dc483034349bc6cffd14190ccff949ac1f34df5fbf01437b8162719b98", + "12_11933417446186b92f63db5931a275f156485d0aef5ee8e265afb350921bd0b1", + "8_aa181c1d9462e3916cb05669e2c408541edf03dad19a9655b340eb32ddd4d060" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"6eb0ec3a" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"7020bcd6" + }, + "stringNotEndsWithDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":25, + "l":[ + "14_141d3f398885dd06d6c14e6629602125d8cb0dddf2ef85896f682c74abb4bc28" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"d37b6f18" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"91ba1bcb" + }, + "stringNotEqualsCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":29, + "s":"b@configcat.com" + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"3ace20fb" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"09c9725f" + }, + "stringNotStartsWithAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":31, + "l":[ + "u@", + "a@", + "b@" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"3717f2ca" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"1d661433" + }, + "stringNotStartsWithAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":23, + "l":[ + "2_b0ab1063bba4431f95710a8bd9c9e2bcd1cebc09fe6803cf87ee501d045e8ee0", + "2_9206a0a6b987410488c8020650788c597509af05aa5327f92cae1c999c401c34", + "2_d8f5c200e2c54b0f84f7df024576f799f484d0e258c6058142a4993daa5e2998" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"b5ba025e" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"c35929e3" + }, + "stringNotStartsWithDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":23, + "l":[ + "1_061a38ed8d5955d90b88d1b1949512a45032390a2581f3c7e36597f7459a48c7" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"72c4e1ac" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"2b16da78" + }, + "stringStartsWithAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":30, + "l":[ + "u@", + "a@", + "b@" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"9e55f5cf" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"e170a185" + }, + "stringStartsWithAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":22, + "l":[ + "2_52f47d1a193071a304e41812055cc1282d90287e5c993ae159fa08fb1ccd3656", + "2_6e5ae1bb586660088d330ac106bbf6b7f2b43be1ca70dbb766f203b76bc84049", + "2_3edba8b738135ad32e85d8695acb8d8511ac67d83f50da55abd9ba469da88efe" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"1d9b7603" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"dd5b3211" + }, + "stringStartsWithDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":22, + "l":[ + "1_55e0b0566d89dc0fda2323efcfb958c782a4648513bdcc4dc84b044fd34230dd" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"3b409872" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"3659b0fe" + } + } + } + """.trimIndent() + override val tests: Array = arrayOf( + TestCase( + key = "boolTrueIn202304", + defaultValue = true, + returnValue = false, + user = ConfigCatUser("12345", custom = mapOf("Custom1" to "2023.04.10")), + expectedLog = """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/src/jsMain/kotlin/com/configcat/NumberFormatter.kt b/src/jsMain/kotlin/com/configcat/NumberFormatter.kt new file mode 100644 index 00000000..62b38b06 --- /dev/null +++ b/src/jsMain/kotlin/com/configcat/NumberFormatter.kt @@ -0,0 +1,10 @@ +package com.configcat + +internal actual fun doubleToString(doubleToString: Double): String { + // The custom double format rules based on the JS double format. Simple toString call is enough. + return doubleToString.toString() +} + +internal actual fun formatDoubleForLog(doubleToFormat: Double): String { + return commonFormatDoubleForLog(doubleToFormat) +} diff --git a/src/jsMain/kotlin/com/configcat/fetch/HttpRequestBuilder.kt b/src/jsMain/kotlin/com/configcat/fetch/HttpRequestBuilder.kt new file mode 100644 index 00000000..53ce0044 --- /dev/null +++ b/src/jsMain/kotlin/com/configcat/fetch/HttpRequestBuilder.kt @@ -0,0 +1,14 @@ +package com.configcat.fetch + +import io.ktor.client.request.* + +internal actual fun httpRequestBuilder( + configCatUserAgent: String, + eTag: String +): HttpRequestBuilder { + val httpRequestBuilder = HttpRequestBuilder() + httpRequestBuilder.url.parameters.append("sdk", configCatUserAgent) + if (eTag.isNotEmpty()) httpRequestBuilder.url.parameters.append("ccetag", eTag) + + return httpRequestBuilder +} diff --git a/src/jsMain/kotlin/com/configcat/override/SettingConverter.kt b/src/jsMain/kotlin/com/configcat/override/SettingConverter.kt new file mode 100644 index 00000000..c98667da --- /dev/null +++ b/src/jsMain/kotlin/com/configcat/override/SettingConverter.kt @@ -0,0 +1,26 @@ +package com.configcat.override + +import com.configcat.model.Setting + +internal actual fun convertToSetting(value: Any): Setting { + val setting = Setting() + when (value) { + is Boolean -> { + setting.settingValue.booleanValue = value + setting.type = 0 + } + + is Double -> { + // is Double return true for Int as well in JS platform + setting.settingValue.doubleValue = value + setting.settingValue.integerValue = value.toInt() + setting.type = -1 + } + + else -> { + setting.settingValue.stringValue = value.toString() + setting.type = 1 + } + } + return setting +} diff --git a/src/jsTest/kotlin/com/configcat/JSConfigV2EvaluationTest.kt b/src/jsTest/kotlin/com/configcat/JSConfigV2EvaluationTest.kt new file mode 100644 index 00000000..4a3b7894 --- /dev/null +++ b/src/jsTest/kotlin/com/configcat/JSConfigV2EvaluationTest.kt @@ -0,0 +1,630 @@ +package com.configcat + +import com.configcat.evaluation.EvaluationTestLogger +import com.configcat.evaluation.LogEvent +import com.configcat.log.LogLevel +import com.configcat.override.OverrideBehavior +import com.configcat.override.OverrideDataSource +import io.ktor.client.engine.mock.* +import io.ktor.http.* +import io.ktor.util.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertContains +import kotlin.test.assertEquals + +@OptIn(ExperimentalCoroutinesApi::class) +class JSConfigV2EvaluationTest { + + // This is the same test cases as the ConfigV2EvaluationTest with different expected results. + // The JS format the double differently, then the other platforms + @Test + fun prerequisiteFlagTypeMismatchTest() = runTest { + runPrerequisiteFlagTypeMismatchTest("stringDependsOnBool", "mainBoolFlag", true, "Dog") + runPrerequisiteFlagTypeMismatchTest("stringDependsOnBool", "mainBoolFlag", false, "Cat") + runPrerequisiteFlagTypeMismatchTest("stringDependsOnBool", "mainBoolFlag", "1", "") + runPrerequisiteFlagTypeMismatchTest("stringDependsOnBool", "mainBoolFlag", 1, "") + runPrerequisiteFlagTypeMismatchTest("stringDependsOnBool", "mainBoolFlag", 1.0, "") + runPrerequisiteFlagTypeMismatchTest("stringDependsOnString", "mainStringFlag", "private", "Dog") + runPrerequisiteFlagTypeMismatchTest("stringDependsOnString", "mainStringFlag", "Private", "Cat") + runPrerequisiteFlagTypeMismatchTest("stringDependsOnString", "mainStringFlag", true, "") + runPrerequisiteFlagTypeMismatchTest("stringDependsOnString", "mainStringFlag", 1, "") + runPrerequisiteFlagTypeMismatchTest("stringDependsOnString", "mainStringFlag", 1.0, "") + runPrerequisiteFlagTypeMismatchTest("stringDependsOnInt", "mainIntFlag", 2, "Dog") + runPrerequisiteFlagTypeMismatchTest("stringDependsOnInt", "mainIntFlag", 1, "Cat") + runPrerequisiteFlagTypeMismatchTest("stringDependsOnInt", "mainIntFlag", "2", "") + runPrerequisiteFlagTypeMismatchTest("stringDependsOnInt", "mainIntFlag", true, "") + // in js the Double converted to Int and vice versa 2.0 == 2 and 2 results Dog + runPrerequisiteFlagTypeMismatchTest("stringDependsOnInt", "mainIntFlag", 2.0, "Dog") + runPrerequisiteFlagTypeMismatchTest("stringDependsOnDouble", "mainDoubleFlag", 0.1, "Dog") + runPrerequisiteFlagTypeMismatchTest("stringDependsOnDouble", "mainDoubleFlag", 0.11, "Cat") + runPrerequisiteFlagTypeMismatchTest("stringDependsOnDouble", "mainDoubleFlag", "0.1", "") + runPrerequisiteFlagTypeMismatchTest("stringDependsOnDouble", "mainDoubleFlag", true, "") + // in js the Double converted to Int and vice versa 1 == 1.0 and 1.0 results Dog + runPrerequisiteFlagTypeMismatchTest("stringDependsOnDouble", "mainDoubleFlag", 1, "Cat") + } + + private suspend fun runPrerequisiteFlagTypeMismatchTest( + key: String, + prerequisiteFlagKey: String, + prerequisiteFlagValue: Any, + expectedValue: String? + ) { + val evaluationTestLogger = EvaluationTestLogger() + + val mockEngine = MockEngine { + respond(content = prerequisiteFlagMismatchRemoteJson, status = HttpStatusCode.OK) + } + val flagOverrideMap = mutableMapOf() + flagOverrideMap[prerequisiteFlagKey] = prerequisiteFlagValue + + val client = ConfigCatClient("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/JoGwdqJZQ0K2xDy7LnbyOg") { + pollingMode = manualPoll() + configCache = SingleValueCache("") + httpEngine = mockEngine + logLevel = LogLevel.ERROR + logger = evaluationTestLogger + flagOverrides = { + behavior = OverrideBehavior.LOCAL_OVER_REMOTE + dataSource = OverrideDataSource.map( + flagOverrideMap + ) + } + } + client.forceRefresh() + + val value = client.getValue(key, "", null) + var errorLogs = mutableListOf() + assertEquals( + expectedValue, + value, + "Flag key: $key PrerequisiteFlagKey: $prerequisiteFlagKey PrerequisiteFlagValue: $prerequisiteFlagValue" + ) + if (expectedValue.isNullOrEmpty()) { + val logsList = evaluationTestLogger.getLogList() + for (i in logsList.indices) { + var log = logsList[i] + if (log.logLevel == LogLevel.ERROR) { + errorLogs.add(log) + } + } + assertEquals(1, errorLogs.size, "Error size not matching") + val errorMessage: String = errorLogs[0].logMessage + assertContains(errorMessage, "[1002]") + + if (prerequisiteFlagValue == null) { + assertContains(errorMessage, "Setting value is null") + } else { + assertContains(errorMessage, "Type mismatch between comparison value") + } + + evaluationTestLogger.resetLogList() + } + + ConfigCatClient.closeAll() + } + + private val prerequisiteFlagMismatchRemoteJson = """ + { + "p":{ + "u":"https://cdn-global.configcat.com", + "r":0, + "s":"PBMv8zBDvXO9ZObbLwsP5TQOsgn8aOv1K3\u002BxPFJCoAU=" + }, + "f":{ + "boolDependsOnBool":{ + "t":0, + "r":[ + { + "c":[ + { + "p":{ + "f":"mainBoolFlag", + "c":0, + "v":{ + "b":true + } + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"8dc94c1d" + } + } + ], + "v":{ + "b":false + }, + "i":"d6194760" + }, + "boolDependsOnBoolDependsOnBool":{ + "t":0, + "r":[ + { + "c":[ + { + "p":{ + "f":"boolDependsOnBool", + "c":0, + "v":{ + "b":true + } + } + } + ], + "s":{ + "v":{ + "b":false + }, + "i":"d6870486" + } + } + ], + "v":{ + "b":true + }, + "i":"cd4c95e7" + }, + "boolDependsOnBoolInverse":{ + "t":0, + "r":[ + { + "c":[ + { + "p":{ + "f":"mainBoolFlagInverse", + "c":1, + "v":{ + "b":true + } + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"3c09bff0" + } + } + ], + "v":{ + "b":false + }, + "i":"cecbc501" + }, + "doubleDependsOnBool":{ + "t":3, + "r":[ + { + "c":[ + { + "p":{ + "f":"mainBoolFlag", + "c":0, + "v":{ + "b":true + } + } + } + ], + "s":{ + "v":{ + "d":1.1 + }, + "i":"271fd003" + } + } + ], + "v":{ + "d":3.14 + }, + "i":"718aae2b" + }, + "intDependsOnBool":{ + "t":2, + "r":[ + { + "c":[ + { + "p":{ + "f":"mainBoolFlag", + "c":0, + "v":{ + "b":true + } + } + } + ], + "s":{ + "v":{ + "i":1 + }, + "i":"d2dda649" + } + } + ], + "v":{ + "i":42 + }, + "i":"43ec49a8" + }, + "mainBoolFlag":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "21_32abe94b0866402b226383eb666a98312dc898119e2a9241ffbfcc114eb6a57b" + ] + } + } + ], + "s":{ + "v":{ + "b":false + }, + "i":"e842ea6f" + } + } + ], + "v":{ + "b":true + }, + "i":"8a68b064" + }, + "mainBoolFlagEmpty":{ + "t":0, + "v":{ + "b":true + }, + "i":"f3295d43" + }, + "mainBoolFlagInverse":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "21_69627ce988f31d14807ed75022d5325645914dadc3bfe7cdc1b6dbeca8763b67" + ] + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"28c65f1f" + } + } + ], + "v":{ + "b":false + }, + "i":"d70e47a7" + }, + "mainDoubleFlag":{ + "t":3, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "21_4cb521a31b1b604875ec3c7c90553a7cb692434f9aee8a318215f9bf1165f0e3" + ] + } + } + ], + "s":{ + "v":{ + "d":0.1 + }, + "i":"a67947ed" + } + } + ], + "v":{ + "d":3.14 + }, + "i":"beb3acc7" + }, + "mainIntFlag":{ + "t":2, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "21_0ad4d095ab7ae197936c7dde2a53e55b2df616c0845c9b216ade6f14b2a4cf3d" + ] + } + } + ], + "s":{ + "v":{ + "i":2 + }, + "i":"67e14078" + } + } + ], + "v":{ + "i":42 + }, + "i":"a7490aca" + }, + "mainStringFlag":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "21_78d8c5a677414bd170650ec60b51e9325663ef8447b280862ec52be49cca7b0f" + ] + } + } + ], + "s":{ + "v":{ + "s":"private" + }, + "i":"51b57fb0" + } + } + ], + "v":{ + "s":"public" + }, + "i":"24c96275" + }, + "stringDependsOnBool":{ + "t":1, + "r":[ + { + "c":[ + { + "p":{ + "f":"mainBoolFlag", + "c":0, + "v":{ + "b":true + } + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"fc8daf80" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"d53a2b42" + }, + "stringDependsOnDouble":{ + "t":1, + "r":[ + { + "c":[ + { + "p":{ + "f":"mainDoubleFlag", + "c":0, + "v":{ + "d":0.1 + } + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"84fc7ed9" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"9cc8fd8f" + }, + "stringDependsOnDoubleIntValue":{ + "t":1, + "r":[ + { + "c":[ + { + "p":{ + "f":"mainDoubleFlag", + "c":0, + "v":{ + "d":0 + } + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"842c1d75" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"db7f56c8" + }, + "stringDependsOnEmptyBool":{ + "t":1, + "r":[ + { + "c":[ + { + "p":{ + "f":"mainBoolFlagEmpty", + "c":0, + "v":{ + "b":true + } + } + } + ], + "s":{ + "v":{ + "s":"EmptyOn" + }, + "i":"d5508c78" + } + } + ], + "v":{ + "s":"EmptyOff" + }, + "i":"8e0dbe88" + }, + "stringDependsOnInt":{ + "t":1, + "r":[ + { + "c":[ + { + "p":{ + "f":"mainIntFlag", + "c":0, + "v":{ + "i":2 + } + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"12531eec" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"e227d926" + }, + "stringDependsOnString":{ + "t":1, + "r":[ + { + "c":[ + { + "p":{ + "f":"mainStringFlag", + "c":0, + "v":{ + "s":"private" + } + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"426b6d4d" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"d36000e1" + }, + "stringDependsOnStringCaseCheck":{ + "t":1, + "r":[ + { + "c":[ + { + "p":{ + "f":"mainStringFlag", + "c":0, + "v":{ + "s":"Private" + } + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"87d24aed" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"ad94f385" + }, + "stringInverseDependsOnEmptyBool":{ + "t":1, + "r":[ + { + "c":[ + { + "p":{ + "f":"mainBoolFlagEmpty", + "c":1, + "v":{ + "b":true + } + } + } + ], + "s":{ + "v":{ + "s":"EmptyOff" + }, + "i":"b7c3efae" + } + } + ], + "v":{ + "s":"EmptyOn" + }, + "i":"f6b4b8a2" + } + } + } + """.trimIndent() +} diff --git a/src/jsTest/kotlin/com/configcat/JsConfigFetcherTests.kt b/src/jsTest/kotlin/com/configcat/JsConfigFetcherTests.kt new file mode 100644 index 00000000..2c493fce --- /dev/null +++ b/src/jsTest/kotlin/com/configcat/JsConfigFetcherTests.kt @@ -0,0 +1,83 @@ +package com.configcat + +import io.ktor.client.engine.mock.* +import io.ktor.http.* +import io.ktor.util.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import kotlin.test.* + +@OptIn(ExperimentalCoroutinesApi::class) +class JsConfigFetcherTests { + @AfterTest + fun tearDown() { + Services.reset() + } + + @Test + fun testFetchParams() = runTest { + val eTag = "test" + val mockEngine = MockEngine.create { + this.addHandler { + respond(content = testBody, status = HttpStatusCode.OK, headersOf(Pair("ETag", listOf(eTag)))) + } + this.addHandler { + respond(content = "", status = HttpStatusCode.NotModified) + } + } as MockEngine + val fetcher = Services.createFetcher(mockEngine) + fetcher.fetch("") + + assertEquals(1, mockEngine.requestHistory.size) + assertEquals( + "ConfigCat-Kotlin/a-${Constants.version}", + mockEngine.requestHistory.last().url.parameters["sdk"] + ) + assertEquals(null, mockEngine.requestHistory.last().url.parameters["ccetag"]) + + fetcher.fetch(eTag) + + assertEquals(2, mockEngine.requestHistory.size) + assertEquals( + "ConfigCat-Kotlin/a-${Constants.version}", + mockEngine.requestHistory.last().url.parameters["sdk"] + ) + assertEquals(eTag, mockEngine.requestHistory.last().url.parameters["ccetag"]) + } + + @Test + fun testFetchParamsWithHTTP2Headers() = runTest { + val eTag = "test" + val mockEngine = MockEngine.create { + this.addHandler { + respond(content = testBody, status = HttpStatusCode.OK, headersOf(Pair("etag", listOf(eTag)))) + } + this.addHandler { + respond(content = "", status = HttpStatusCode.NotModified) + } + } as MockEngine + val fetcher = Services.createFetcher(mockEngine) + fetcher.fetch("") + + assertEquals(1, mockEngine.requestHistory.size) + assertEquals( + "ConfigCat-Kotlin/a-${Constants.version}", + mockEngine.requestHistory.last().url.parameters["sdk"] + ) + assertEquals(null, mockEngine.requestHistory.last().url.parameters["ccetag"]) + + fetcher.fetch(eTag) + + assertEquals(2, mockEngine.requestHistory.size) + assertEquals( + "ConfigCat-Kotlin/a-${Constants.version}", + mockEngine.requestHistory.last().url.parameters["sdk"] + ) + assertEquals(eTag, mockEngine.requestHistory.last().url.parameters["ccetag"]) + } + + companion object { + const val testBody = + """{ "p": { "u": "https://cdn-global.configcat.com", "s": "test-slat" }, "f": { "fakeKey": { "t": 1, "v": {"s": "fakeValue" }, "p": [], "r": [], "a":""} }, "s": [] }""" + } +} diff --git a/src/jvmMain/kotlin/com/configcat/NumberFormatter.kt b/src/jvmMain/kotlin/com/configcat/NumberFormatter.kt new file mode 100644 index 00000000..b342f890 --- /dev/null +++ b/src/jvmMain/kotlin/com/configcat/NumberFormatter.kt @@ -0,0 +1,35 @@ +package com.configcat + +import java.text.DecimalFormat +import java.text.DecimalFormatSymbols +import java.util.* +import kotlin.math.abs + +internal actual fun doubleToString(doubleToString: Double): String { + // Handle Double.NaN, Double.POSITIVE_INFINITY and Double.NEGATIVE_INFINITY + if (doubleToString.isNaN() || doubleToString.isInfinite()) { + return doubleToString.toString() + } + + // To get similar result between different SDKs the Double value format is modified. + // Between 1e-6 and 1e21 we don't use scientific-notation. Over these limits scientific-notation used but the + // ExponentSeparator replaced with "e" and "e+". + // "." used as decimal separator in all cases. + val abs = abs(doubleToString) + val fmt = + if (1e-6 <= abs && abs < 1e21) DecimalFormat("#.#################") else DecimalFormat("#.#################E0") + val symbols = DecimalFormatSymbols.getInstance(Locale.UK) + if (abs > 1) { + symbols.exponentSeparator = "e+" + } else { + symbols.exponentSeparator = "e" + } + fmt.decimalFormatSymbols = symbols + return fmt.format(doubleToString) +} + +internal actual fun formatDoubleForLog(doubleToFormat: Double): String { + val decimalFormat = DecimalFormat("0.#####") + decimalFormat.decimalFormatSymbols = DecimalFormatSymbols.getInstance(Locale.UK) + return decimalFormat.format(doubleToFormat) +} diff --git a/src/jvmMain/kotlin/com/configcat/fetch/HttpRequestBuilder.kt b/src/jvmMain/kotlin/com/configcat/fetch/HttpRequestBuilder.kt new file mode 100644 index 00000000..162c1103 --- /dev/null +++ b/src/jvmMain/kotlin/com/configcat/fetch/HttpRequestBuilder.kt @@ -0,0 +1,10 @@ +package com.configcat.fetch + +import io.ktor.client.request.* + +internal actual fun httpRequestBuilder( + configCatUserAgent: String, + eTag: String +): HttpRequestBuilder { + return commonHttpRequestBuilder(configCatUserAgent, eTag) +} diff --git a/src/jvmMain/kotlin/com/configcat/override/SettingConverter.kt b/src/jvmMain/kotlin/com/configcat/override/SettingConverter.kt new file mode 100644 index 00000000..607baf5c --- /dev/null +++ b/src/jvmMain/kotlin/com/configcat/override/SettingConverter.kt @@ -0,0 +1,7 @@ +package com.configcat.override + +import com.configcat.model.Setting + +internal actual fun convertToSetting(value: Any): Setting { + return commonConvertToSetting(value) +} diff --git a/src/nativeMain/kotlin/com/configcat/NumberFormatter.kt b/src/nativeMain/kotlin/com/configcat/NumberFormatter.kt new file mode 100644 index 00000000..4da72973 --- /dev/null +++ b/src/nativeMain/kotlin/com/configcat/NumberFormatter.kt @@ -0,0 +1,9 @@ +package com.configcat + +internal actual fun doubleToString(doubleToString: Double): String { + return commonDoubleToString(doubleToString) +} + +internal actual fun formatDoubleForLog(doubleToFormat: Double): String { + return commonFormatDoubleForLog(doubleToFormat) +} diff --git a/src/nativeMain/kotlin/com/configcat/fetch/HttpRequestBuilder.kt b/src/nativeMain/kotlin/com/configcat/fetch/HttpRequestBuilder.kt new file mode 100644 index 00000000..162c1103 --- /dev/null +++ b/src/nativeMain/kotlin/com/configcat/fetch/HttpRequestBuilder.kt @@ -0,0 +1,10 @@ +package com.configcat.fetch + +import io.ktor.client.request.* + +internal actual fun httpRequestBuilder( + configCatUserAgent: String, + eTag: String +): HttpRequestBuilder { + return commonHttpRequestBuilder(configCatUserAgent, eTag) +} diff --git a/src/nativeMain/kotlin/com/configcat/override/SettingConverter.kt b/src/nativeMain/kotlin/com/configcat/override/SettingConverter.kt new file mode 100644 index 00000000..607baf5c --- /dev/null +++ b/src/nativeMain/kotlin/com/configcat/override/SettingConverter.kt @@ -0,0 +1,7 @@ +package com.configcat.override + +import com.configcat.model.Setting + +internal actual fun convertToSetting(value: Any): Setting { + return commonConvertToSetting(value) +} diff --git a/src/nativeTest/kotlin/com/configcat/NativeEvaluationTests.kt b/src/nativeTest/kotlin/com/configcat/NativeEvaluationTests.kt new file mode 100644 index 00000000..87a3393c --- /dev/null +++ b/src/nativeTest/kotlin/com/configcat/NativeEvaluationTests.kt @@ -0,0 +1,92 @@ +package com.configcat + +import com.configcat.data.NativeComparatorsTests +import com.configcat.data.NativeEpochDateValidationTests +import com.configcat.evaluation.EvaluationTestLogger +import com.configcat.evaluation.data.* +import com.configcat.log.LogLevel +import io.ktor.client.engine.mock.* +import io.ktor.http.* +import io.ktor.util.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.fail + +/** + * Run the Evaluation test cases where double format used. This tests cases has a different expected value. + */ +@OptIn(ExperimentalCoroutinesApi::class) +class NativeEvaluationTests { + + @Test + fun testComparators() = runTest { + // The test contains formatted double value, which is different in case of JS module + testEvaluation(NativeComparatorsTests) + } + + @Test + fun testEpochDateValidation() = runTest { + // The test contains formatted double value, which is different in case of JS module + testEvaluation(NativeEpochDateValidationTests) + } + + private suspend fun testEvaluation(testSet: TestSet) { + var sdkKey = testSet.sdkKey + if (sdkKey.isNullOrEmpty()) { + sdkKey = TEST_SDK_KEY + } + + val mockEngine = MockEngine { + respond( + content = testSet.jsonOverride, + status = HttpStatusCode.OK, + headersOf(Pair("ETag", listOf("fakeETag"))) + ) + } + + val evaluationTestLogger = EvaluationTestLogger() + val client = ConfigCatClient(sdkKey) { + pollingMode = manualPoll() + baseUrl = testSet.baseUrl + httpEngine = mockEngine + logger = evaluationTestLogger + logLevel = LogLevel.INFO + } + client.forceRefresh() + + val tests = testSet.tests + val errors: ArrayList = arrayListOf() + for (test in tests!!) { + val settingKey = test.key + + val result: Any? = client.getAnyValue(settingKey, test.defaultValue, test.user) + if (test.returnValue != result) { + errors.add("Return value mismatch for test: %s Test Key: $settingKey Expected: ${test.returnValue}, Result: $result \n") + } + val expectedLog = test.expectedLog + val logResultBuilder = StringBuilder() + val logsList = evaluationTestLogger.getLogList() + for (i in logsList.indices) { + val log = logsList[i] + logResultBuilder.append(log.logMessage) + if (i != logsList.size - 1) { + logResultBuilder.append("\n") + } + } + val logResult: String = logResultBuilder.toString() + if (expectedLog != logResult) { + errors.add("Log mismatch for test: %s Test Key: $settingKey Expected:\n$expectedLog\nResult:\n$logResult\n") + } + evaluationTestLogger.resetLogList() + } + if (errors.isNotEmpty()) { + fail(errors.joinToString("\n")) + } + client.close() + } + + companion object { + private const val TEST_SDK_KEY = "configcat-sdk-test-key/0000000000000000000000" + } +} diff --git a/src/nativeTest/kotlin/com/configcat/data/NativeComparatorsTests.kt b/src/nativeTest/kotlin/com/configcat/data/NativeComparatorsTests.kt new file mode 100644 index 00000000..074e4605 --- /dev/null +++ b/src/nativeTest/kotlin/com/configcat/data/NativeComparatorsTests.kt @@ -0,0 +1,1459 @@ +package com.configcat.data + +import com.configcat.ConfigCatUser +import com.configcat.evaluation.data.TestCase +import com.configcat.evaluation.data.TestSet + +object NativeComparatorsTests : TestSet { + override val sdkKey = "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ" + override val baseUrl = null + override val jsonOverride = """ + { + "p":{ + "u":"https://cdn-global.configcat.com", + "r":0, + "s":"JEl\u002BhoGfr/01JCnpxr7kOCIoB2bYAM3uTMShm6HiAc4=" + }, + "f":{ + "allinone":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":20, + "s":"0740e9103e26acb85b086dd9a15eaa84246902f096655371a5f001b9e13754e8" + } + }, + { + "u":{ + "a":"Email", + "c":21, + "s":"0740e9103e26acb85b086dd9a15eaa84246902f096655371a5f001b9e13754e8" + } + } + ], + "s":{ + "v":{ + "s":"1h" + }, + "i":"e3a79156" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":28, + "s":"joe@example.com" + } + }, + { + "u":{ + "a":"Email", + "c":29, + "s":"joe@example.com" + } + } + ], + "s":{ + "v":{ + "s":"1c" + }, + "i":"ed60451a" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":16, + "l":[ + "0740e9103e26acb85b086dd9a15eaa84246902f096655371a5f001b9e13754e8" + ] + } + }, + { + "u":{ + "a":"Email", + "c":17, + "l":[ + "0740e9103e26acb85b086dd9a15eaa84246902f096655371a5f001b9e13754e8" + ] + } + } + ], + "s":{ + "v":{ + "s":"2h" + }, + "i":"aa24b7a3" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":0, + "l":[ + "joe@example.com" + ] + } + }, + { + "u":{ + "a":"Email", + "c":1, + "l":[ + "joe@example.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"2c" + }, + "i":"d37425a1" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":22, + "l":[ + "4_e99c716658ca0b1035394161a3ca54f8dc688930ad90bed26aeff075cb947397" + ] + } + }, + { + "u":{ + "a":"Email", + "c":23, + "l":[ + "4_e99c716658ca0b1035394161a3ca54f8dc688930ad90bed26aeff075cb947397" + ] + } + } + ], + "s":{ + "v":{ + "s":"3h" + }, + "i":"5e6e0c6c" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":30, + "l":[ + "joe@" + ] + } + }, + { + "u":{ + "a":"Email", + "c":31, + "l":[ + "joe@" + ] + } + } + ], + "s":{ + "v":{ + "s":"3c" + }, + "i":"5f562a70" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "12_29030906a5c2729247ccad10154b56b84d61ee4d732361e0ba7c3817da4f91b3" + ] + } + }, + { + "u":{ + "a":"Email", + "c":25, + "l":[ + "12_29030906a5c2729247ccad10154b56b84d61ee4d732361e0ba7c3817da4f91b3" + ] + } + } + ], + "s":{ + "v":{ + "s":"4h" + }, + "i":"91b91d69" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":32, + "l":[ + "@example.com" + ] + } + }, + { + "u":{ + "a":"Email", + "c":33, + "l":[ + "@example.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"4c" + }, + "i":"4c80a977" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "e@e" + ] + } + }, + { + "u":{ + "a":"Email", + "c":3, + "l":[ + "e@e" + ] + } + } + ], + "s":{ + "v":{ + "s":"5" + }, + "i":"dd12c429" + } + }, + { + "c":[ + { + "u":{ + "a":"Version", + "c":4, + "l":[ + "1.0.0" + ] + } + }, + { + "u":{ + "a":"Version", + "c":5, + "l":[ + "1.0.0" + ] + } + } + ], + "s":{ + "v":{ + "s":"6" + }, + "i":"dba5d266" + } + }, + { + "c":[ + { + "u":{ + "a":"Version", + "c":6, + "s":"1.0.1" + } + }, + { + "u":{ + "a":"Version", + "c":9, + "s":"1.0.1" + } + } + ], + "s":{ + "v":{ + "s":"7" + }, + "i":"1637ffc5" + } + }, + { + "c":[ + { + "u":{ + "a":"Version", + "c":8, + "s":"0.9.9" + } + }, + { + "u":{ + "a":"Version", + "c":7, + "s":"0.9.9" + } + } + ], + "s":{ + "v":{ + "s":"8" + }, + "i":"b084ddd6" + } + }, + { + "c":[ + { + "u":{ + "a":"Number", + "c":10, + "d":1 + } + }, + { + "u":{ + "a":"Number", + "c":11, + "d":1 + } + } + ], + "s":{ + "v":{ + "s":"9" + }, + "i":"d1d537a6" + } + }, + { + "c":[ + { + "u":{ + "a":"Number", + "c":12, + "d":1.1 + } + }, + { + "u":{ + "a":"Number", + "c":15, + "d":1.1 + } + } + ], + "s":{ + "v":{ + "s":"10" + }, + "i":"52c846d0" + } + }, + { + "c":[ + { + "u":{ + "a":"Number", + "c":14, + "d":0.9 + } + }, + { + "u":{ + "a":"Number", + "c":13, + "d":0.9 + } + } + ], + "s":{ + "v":{ + "s":"11" + }, + "i":"c91ffb7c" + } + }, + { + "c":[ + { + "u":{ + "a":"Date", + "c":18, + "d":1693497600 + } + }, + { + "u":{ + "a":"Date", + "c":19, + "d":1693497600 + } + } + ], + "s":{ + "v":{ + "s":"12" + }, + "i":"c12182ef" + } + }, + { + "c":[ + { + "u":{ + "a":"Country", + "c":26, + "l":[ + "5a85699e7343a36d89ee75dca859f7a73cb6be89182095bffb021d1d78de046c" + ] + } + }, + { + "u":{ + "a":"Country", + "c":27, + "l":[ + "5a85699e7343a36d89ee75dca859f7a73cb6be89182095bffb021d1d78de046c" + ] + } + } + ], + "s":{ + "v":{ + "s":"13h" + }, + "i":"a16b1a17" + } + }, + { + "c":[ + { + "u":{ + "a":"Country", + "c":34, + "l":[ + "USA" + ] + } + }, + { + "u":{ + "a":"Country", + "c":35, + "l":[ + "USA" + ] + } + } + ], + "s":{ + "v":{ + "s":"13c" + }, + "i":"1a17d1b3" + } + } + ], + "v":{ + "s":"default" + }, + "i":"9ff25f81" + }, + "arrayContainsCaseCheckDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":26, + "l":[ + "00083f86e0f648b23f6721d43033bbef14378266fbf7de8a6760cc2ad237e9f3" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"5d80eff1" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"ce055a38" + }, + "arrayContainsDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":26, + "l":[ + "c2d5024661bc0e13f769cbb28bbfab7b78dac88b7876f007020f2e7cd47b1114" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"147fdd01" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"5f573f9c" + }, + "arrayDoesNotContainCaseCheckDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":27, + "l":[ + "14748140c64ecab48c7fd13f03811fe6390c8f578c99df96cf36fc2c6152f660" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"d4ad5730" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"df4915fd" + }, + "arrayDoesNotContainDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":27, + "l":[ + "bc0c24462c5098e434c63c7fcc6343af5000a4e7affd309f365edd4ccb7f428b" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"c2161ac9" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"41910880" + }, + "boolTrueIn202304":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":19, + "d":1680307200 + } + }, + { + "u":{ + "a":"Custom1", + "c":18, + "d":1682899200 + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"6948d7cd" + } + } + ], + "v":{ + "b":false + }, + "i":"ae2a09bd" + }, + "countryPercentageAttribute":{ + "t":1, + "a":"Country", + "p":[ + { + "p":50, + "v":{ + "s":"Falcon" + }, + "i":"2b05fd81" + }, + { + "p":50, + "v":{ + "s":"Horse" + }, + "i":"e28b6a82" + } + ], + "v":{ + "s":"Chicken" + }, + "i":"29bb6bbb" + }, + "customPercentageAttribute":{ + "t":1, + "a":"Custom1", + "p":[ + { + "p":50, + "v":{ + "s":"Falcon" + }, + "i":"3715712d" + }, + { + "p":50, + "v":{ + "s":"Horse" + }, + "i":"7b3542d5" + } + ], + "v":{ + "s":"Chicken" + }, + "i":"50466fb6" + }, + "missingPercentageAttribute":{ + "t":1, + "a":"NotFound", + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "14_902a42101e8b77851c98456b383fd959ed0f5aed5b919b4a623c8c756cf0c3ab" + ] + } + } + ], + "p":[ + { + "p":50, + "v":{ + "s":"Falcon" + }, + "i":"4b7d88ba" + }, + { + "p":50, + "v":{ + "s":"Horse" + }, + "i":"a1c2c9a9" + } + ] + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "14_902a42101e8b77851c98456b383fd959ed0f5aed5b919b4a623c8c756cf0c3ab" + ] + } + } + ], + "s":{ + "v":{ + "s":"NotFound" + }, + "i":"8aa042fe" + } + } + ], + "v":{ + "s":"Chicken" + }, + "i":"e5107172" + }, + "stringArrayContainsAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":34, + "l":[ + "read", + "write", + "execute" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"9ddb8a37" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"0d45ab4b" + }, + "stringArrayContainsAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":26, + "l":[ + "090415ce6b462a2152e06d68aeb7c452a564d19a262eb959c510636a189e105d", + "0aa50b49ca02a59cf507856d88ab25b76a7e69e553d25e67254359d8bbb8b1d6", + "1aa91982f7ccae1943f05ac437d635b3261e3ce06aba846dd2ecc6332e4675c2" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"aa03b1ff" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"203317f5" + }, + "stringArrayNotContainsAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":35, + "l":[ + "read", + "write", + "execute" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"15c865df" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"6df210da" + }, + "stringArrayNotContainsAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":27, + "l":[ + "b645a36d4d2e24a0612296ded074eace87ce10a0da21c5cb2ac9a6dccbe79cc5", + "552a92975423ea255384f48a6387be172c05ac72238f90050cdfe27cc4659cfd", + "4c00d4171202dbeb35830b1df8e47185968351c771bb2f633ea50ac1c049e016" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"259816ba" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"60b961b0" + }, + "stringContainsAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@verizon.com", + "@verizon.net", + "@aol.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"09af657f" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"063bcf39" + }, + "stringDoseNotEqualDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":21, + "s":"3cb496a1c3844215c784116ce9e91c143860d7ef18f7fadadcc901b8df5d235c" + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"8e423808" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"1835a09a" + }, + "stringEndsWithAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":32, + "l":[ + "@verizon.com", + "@verizon.net", + "@aol.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"33d35402" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"31976ec3" + }, + "stringEndsWithAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "12_781f5f9b054a14721a835fcdd2d03ac6d45d99eb55b387bf5938904c8f65aa35", + "12_d15f199cfbd96ef1c6f7683e66d5e0f85c9c591ce377289abb2e785fc71bbdf1", + "8_6ee234c513b7518bd705cc1049576c1e757a201200ff26a6a3a91821f954c6cb" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"7231ddf8" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"de17fd2a" + }, + "stringEndsWithDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "14_236ec291c9b54fad3373a0b7e0e465b33459198fd1027838c101516ad8ae1b39" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"d7a00741" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"45b7d922" + }, + "stringEqualsCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":28, + "s":"a@configcat.com" + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"087e01dd" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"89785ab3" + }, + "stringEqualsDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":20, + "s":"86e6b430939acac093f3d8c48be10798896f0abf3b96e2e39080acedd925d887" + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"703c31ed" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"adc0b01c" + }, + "stringNotContainsAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":3, + "l":[ + "@verizon.com", + "@verizon.net", + "@aol.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"49627b36" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"36848b03" + }, + "stringNotEndsWithAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":33, + "l":[ + "@verizon.com", + "@verizon.net", + "@aol.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"886ced9d" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"864b6202" + }, + "stringNotEndsWithAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":25, + "l":[ + "12_295e66dc483034349bc6cffd14190ccff949ac1f34df5fbf01437b8162719b98", + "12_11933417446186b92f63db5931a275f156485d0aef5ee8e265afb350921bd0b1", + "8_aa181c1d9462e3916cb05669e2c408541edf03dad19a9655b340eb32ddd4d060" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"6eb0ec3a" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"7020bcd6" + }, + "stringNotEndsWithDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":25, + "l":[ + "14_141d3f398885dd06d6c14e6629602125d8cb0dddf2ef85896f682c74abb4bc28" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"d37b6f18" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"91ba1bcb" + }, + "stringNotEqualsCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":29, + "s":"b@configcat.com" + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"3ace20fb" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"09c9725f" + }, + "stringNotStartsWithAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":31, + "l":[ + "u@", + "a@", + "b@" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"3717f2ca" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"1d661433" + }, + "stringNotStartsWithAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":23, + "l":[ + "2_b0ab1063bba4431f95710a8bd9c9e2bcd1cebc09fe6803cf87ee501d045e8ee0", + "2_9206a0a6b987410488c8020650788c597509af05aa5327f92cae1c999c401c34", + "2_d8f5c200e2c54b0f84f7df024576f799f484d0e258c6058142a4993daa5e2998" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"b5ba025e" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"c35929e3" + }, + "stringNotStartsWithDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":23, + "l":[ + "1_061a38ed8d5955d90b88d1b1949512a45032390a2581f3c7e36597f7459a48c7" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"72c4e1ac" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"2b16da78" + }, + "stringStartsWithAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":30, + "l":[ + "u@", + "a@", + "b@" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"9e55f5cf" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"e170a185" + }, + "stringStartsWithAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":22, + "l":[ + "2_52f47d1a193071a304e41812055cc1282d90287e5c993ae159fa08fb1ccd3656", + "2_6e5ae1bb586660088d330ac106bbf6b7f2b43be1ca70dbb766f203b76bc84049", + "2_3edba8b738135ad32e85d8695acb8d8511ac67d83f50da55abd9ba469da88efe" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"1d9b7603" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"dd5b3211" + }, + "stringStartsWithDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":22, + "l":[ + "1_55e0b0566d89dc0fda2323efcfb958c782a4648513bdcc4dc84b044fd34230dd" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"3b409872" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"3659b0fe" + } + } +} + """.trimIndent() + override val tests: Array = arrayOf( + TestCase( + key = "allinone", + defaultValue = "", + returnValue = "default", + user = ConfigCatUser( + "12345", + "joe@example.com", + "[\"USA\"]", + custom = mapOf("Version" to "1.0.0", "Number" to "1.0", "Date" to "1693497500") + ), + expectedLog = """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 '1.6934976E9' (2023-08-31T16:00:00.000Z UTC) => true + AND User.Date AFTER '1.6934976E9' (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/src/nativeTest/kotlin/com/configcat/data/NativeEpochDateValidationTests.kt b/src/nativeTest/kotlin/com/configcat/data/NativeEpochDateValidationTests.kt new file mode 100644 index 00000000..9c485468 --- /dev/null +++ b/src/nativeTest/kotlin/com/configcat/data/NativeEpochDateValidationTests.kt @@ -0,0 +1,1404 @@ +package com.configcat.data + +import com.configcat.ConfigCatUser +import com.configcat.evaluation.data.TestCase +import com.configcat.evaluation.data.TestSet + +object NativeEpochDateValidationTests : TestSet { + override val sdkKey = "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ" + override val baseUrl = null + override val jsonOverride = """ + { + "p":{ + "u":"https://cdn-global.configcat.com", + "r":0, + "s":"JEl\u002BhoGfr/01JCnpxr7kOCIoB2bYAM3uTMShm6HiAc4=" + }, + "f":{ + "allinone":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":20, + "s":"0740e9103e26acb85b086dd9a15eaa84246902f096655371a5f001b9e13754e8" + } + }, + { + "u":{ + "a":"Email", + "c":21, + "s":"0740e9103e26acb85b086dd9a15eaa84246902f096655371a5f001b9e13754e8" + } + } + ], + "s":{ + "v":{ + "s":"1h" + }, + "i":"e3a79156" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":28, + "s":"joe@example.com" + } + }, + { + "u":{ + "a":"Email", + "c":29, + "s":"joe@example.com" + } + } + ], + "s":{ + "v":{ + "s":"1c" + }, + "i":"ed60451a" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":16, + "l":[ + "0740e9103e26acb85b086dd9a15eaa84246902f096655371a5f001b9e13754e8" + ] + } + }, + { + "u":{ + "a":"Email", + "c":17, + "l":[ + "0740e9103e26acb85b086dd9a15eaa84246902f096655371a5f001b9e13754e8" + ] + } + } + ], + "s":{ + "v":{ + "s":"2h" + }, + "i":"aa24b7a3" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":0, + "l":[ + "joe@example.com" + ] + } + }, + { + "u":{ + "a":"Email", + "c":1, + "l":[ + "joe@example.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"2c" + }, + "i":"d37425a1" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":22, + "l":[ + "4_e99c716658ca0b1035394161a3ca54f8dc688930ad90bed26aeff075cb947397" + ] + } + }, + { + "u":{ + "a":"Email", + "c":23, + "l":[ + "4_e99c716658ca0b1035394161a3ca54f8dc688930ad90bed26aeff075cb947397" + ] + } + } + ], + "s":{ + "v":{ + "s":"3h" + }, + "i":"5e6e0c6c" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":30, + "l":[ + "joe@" + ] + } + }, + { + "u":{ + "a":"Email", + "c":31, + "l":[ + "joe@" + ] + } + } + ], + "s":{ + "v":{ + "s":"3c" + }, + "i":"5f562a70" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "12_29030906a5c2729247ccad10154b56b84d61ee4d732361e0ba7c3817da4f91b3" + ] + } + }, + { + "u":{ + "a":"Email", + "c":25, + "l":[ + "12_29030906a5c2729247ccad10154b56b84d61ee4d732361e0ba7c3817da4f91b3" + ] + } + } + ], + "s":{ + "v":{ + "s":"4h" + }, + "i":"91b91d69" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":32, + "l":[ + "@example.com" + ] + } + }, + { + "u":{ + "a":"Email", + "c":33, + "l":[ + "@example.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"4c" + }, + "i":"4c80a977" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "e@e" + ] + } + }, + { + "u":{ + "a":"Email", + "c":3, + "l":[ + "e@e" + ] + } + } + ], + "s":{ + "v":{ + "s":"5" + }, + "i":"dd12c429" + } + }, + { + "c":[ + { + "u":{ + "a":"Version", + "c":4, + "l":[ + "1.0.0" + ] + } + }, + { + "u":{ + "a":"Version", + "c":5, + "l":[ + "1.0.0" + ] + } + } + ], + "s":{ + "v":{ + "s":"6" + }, + "i":"dba5d266" + } + }, + { + "c":[ + { + "u":{ + "a":"Version", + "c":6, + "s":"1.0.1" + } + }, + { + "u":{ + "a":"Version", + "c":9, + "s":"1.0.1" + } + } + ], + "s":{ + "v":{ + "s":"7" + }, + "i":"1637ffc5" + } + }, + { + "c":[ + { + "u":{ + "a":"Version", + "c":8, + "s":"0.9.9" + } + }, + { + "u":{ + "a":"Version", + "c":7, + "s":"0.9.9" + } + } + ], + "s":{ + "v":{ + "s":"8" + }, + "i":"b084ddd6" + } + }, + { + "c":[ + { + "u":{ + "a":"Number", + "c":10, + "d":1 + } + }, + { + "u":{ + "a":"Number", + "c":11, + "d":1 + } + } + ], + "s":{ + "v":{ + "s":"9" + }, + "i":"d1d537a6" + } + }, + { + "c":[ + { + "u":{ + "a":"Number", + "c":12, + "d":1.1 + } + }, + { + "u":{ + "a":"Number", + "c":15, + "d":1.1 + } + } + ], + "s":{ + "v":{ + "s":"10" + }, + "i":"52c846d0" + } + }, + { + "c":[ + { + "u":{ + "a":"Number", + "c":14, + "d":0.9 + } + }, + { + "u":{ + "a":"Number", + "c":13, + "d":0.9 + } + } + ], + "s":{ + "v":{ + "s":"11" + }, + "i":"c91ffb7c" + } + }, + { + "c":[ + { + "u":{ + "a":"Date", + "c":18, + "d":1693497600 + } + }, + { + "u":{ + "a":"Date", + "c":19, + "d":1693497600 + } + } + ], + "s":{ + "v":{ + "s":"12" + }, + "i":"c12182ef" + } + }, + { + "c":[ + { + "u":{ + "a":"Country", + "c":26, + "l":[ + "5a85699e7343a36d89ee75dca859f7a73cb6be89182095bffb021d1d78de046c" + ] + } + }, + { + "u":{ + "a":"Country", + "c":27, + "l":[ + "5a85699e7343a36d89ee75dca859f7a73cb6be89182095bffb021d1d78de046c" + ] + } + } + ], + "s":{ + "v":{ + "s":"13h" + }, + "i":"a16b1a17" + } + }, + { + "c":[ + { + "u":{ + "a":"Country", + "c":34, + "l":[ + "USA" + ] + } + }, + { + "u":{ + "a":"Country", + "c":35, + "l":[ + "USA" + ] + } + } + ], + "s":{ + "v":{ + "s":"13c" + }, + "i":"1a17d1b3" + } + } + ], + "v":{ + "s":"default" + }, + "i":"9ff25f81" + }, + "arrayContainsCaseCheckDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":26, + "l":[ + "00083f86e0f648b23f6721d43033bbef14378266fbf7de8a6760cc2ad237e9f3" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"5d80eff1" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"ce055a38" + }, + "arrayContainsDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":26, + "l":[ + "c2d5024661bc0e13f769cbb28bbfab7b78dac88b7876f007020f2e7cd47b1114" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"147fdd01" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"5f573f9c" + }, + "arrayDoesNotContainCaseCheckDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":27, + "l":[ + "14748140c64ecab48c7fd13f03811fe6390c8f578c99df96cf36fc2c6152f660" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"d4ad5730" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"df4915fd" + }, + "arrayDoesNotContainDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":27, + "l":[ + "bc0c24462c5098e434c63c7fcc6343af5000a4e7affd309f365edd4ccb7f428b" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"c2161ac9" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"41910880" + }, + "boolTrueIn202304":{ + "t":0, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":19, + "d":1680307200 + } + }, + { + "u":{ + "a":"Custom1", + "c":18, + "d":1682899200 + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"6948d7cd" + } + } + ], + "v":{ + "b":false + }, + "i":"ae2a09bd" + }, + "countryPercentageAttribute":{ + "t":1, + "a":"Country", + "p":[ + { + "p":50, + "v":{ + "s":"Falcon" + }, + "i":"2b05fd81" + }, + { + "p":50, + "v":{ + "s":"Horse" + }, + "i":"e28b6a82" + } + ], + "v":{ + "s":"Chicken" + }, + "i":"29bb6bbb" + }, + "customPercentageAttribute":{ + "t":1, + "a":"Custom1", + "p":[ + { + "p":50, + "v":{ + "s":"Falcon" + }, + "i":"3715712d" + }, + { + "p":50, + "v":{ + "s":"Horse" + }, + "i":"7b3542d5" + } + ], + "v":{ + "s":"Chicken" + }, + "i":"50466fb6" + }, + "missingPercentageAttribute":{ + "t":1, + "a":"NotFound", + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "14_902a42101e8b77851c98456b383fd959ed0f5aed5b919b4a623c8c756cf0c3ab" + ] + } + } + ], + "p":[ + { + "p":50, + "v":{ + "s":"Falcon" + }, + "i":"4b7d88ba" + }, + { + "p":50, + "v":{ + "s":"Horse" + }, + "i":"a1c2c9a9" + } + ] + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "14_902a42101e8b77851c98456b383fd959ed0f5aed5b919b4a623c8c756cf0c3ab" + ] + } + } + ], + "s":{ + "v":{ + "s":"NotFound" + }, + "i":"8aa042fe" + } + } + ], + "v":{ + "s":"Chicken" + }, + "i":"e5107172" + }, + "stringArrayContainsAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":34, + "l":[ + "read", + "write", + "execute" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"9ddb8a37" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"0d45ab4b" + }, + "stringArrayContainsAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":26, + "l":[ + "090415ce6b462a2152e06d68aeb7c452a564d19a262eb959c510636a189e105d", + "0aa50b49ca02a59cf507856d88ab25b76a7e69e553d25e67254359d8bbb8b1d6", + "1aa91982f7ccae1943f05ac437d635b3261e3ce06aba846dd2ecc6332e4675c2" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"aa03b1ff" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"203317f5" + }, + "stringArrayNotContainsAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":35, + "l":[ + "read", + "write", + "execute" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"15c865df" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"6df210da" + }, + "stringArrayNotContainsAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Custom1", + "c":27, + "l":[ + "b645a36d4d2e24a0612296ded074eace87ce10a0da21c5cb2ac9a6dccbe79cc5", + "552a92975423ea255384f48a6387be172c05ac72238f90050cdfe27cc4659cfd", + "4c00d4171202dbeb35830b1df8e47185968351c771bb2f633ea50ac1c049e016" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"259816ba" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"60b961b0" + }, + "stringContainsAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@verizon.com", + "@verizon.net", + "@aol.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"09af657f" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"063bcf39" + }, + "stringDoseNotEqualDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":21, + "s":"3cb496a1c3844215c784116ce9e91c143860d7ef18f7fadadcc901b8df5d235c" + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"8e423808" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"1835a09a" + }, + "stringEndsWithAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":32, + "l":[ + "@verizon.com", + "@verizon.net", + "@aol.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"33d35402" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"31976ec3" + }, + "stringEndsWithAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "12_781f5f9b054a14721a835fcdd2d03ac6d45d99eb55b387bf5938904c8f65aa35", + "12_d15f199cfbd96ef1c6f7683e66d5e0f85c9c591ce377289abb2e785fc71bbdf1", + "8_6ee234c513b7518bd705cc1049576c1e757a201200ff26a6a3a91821f954c6cb" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"7231ddf8" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"de17fd2a" + }, + "stringEndsWithDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":24, + "l":[ + "14_236ec291c9b54fad3373a0b7e0e465b33459198fd1027838c101516ad8ae1b39" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"d7a00741" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"45b7d922" + }, + "stringEqualsCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":28, + "s":"a@configcat.com" + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"087e01dd" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"89785ab3" + }, + "stringEqualsDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":20, + "s":"86e6b430939acac093f3d8c48be10798896f0abf3b96e2e39080acedd925d887" + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"703c31ed" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"adc0b01c" + }, + "stringNotContainsAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":3, + "l":[ + "@verizon.com", + "@verizon.net", + "@aol.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"49627b36" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"36848b03" + }, + "stringNotEndsWithAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":33, + "l":[ + "@verizon.com", + "@verizon.net", + "@aol.com" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"886ced9d" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"864b6202" + }, + "stringNotEndsWithAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":25, + "l":[ + "12_295e66dc483034349bc6cffd14190ccff949ac1f34df5fbf01437b8162719b98", + "12_11933417446186b92f63db5931a275f156485d0aef5ee8e265afb350921bd0b1", + "8_aa181c1d9462e3916cb05669e2c408541edf03dad19a9655b340eb32ddd4d060" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"6eb0ec3a" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"7020bcd6" + }, + "stringNotEndsWithDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":25, + "l":[ + "14_141d3f398885dd06d6c14e6629602125d8cb0dddf2ef85896f682c74abb4bc28" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"d37b6f18" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"91ba1bcb" + }, + "stringNotEqualsCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":29, + "s":"b@configcat.com" + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"3ace20fb" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"09c9725f" + }, + "stringNotStartsWithAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":31, + "l":[ + "u@", + "a@", + "b@" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"3717f2ca" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"1d661433" + }, + "stringNotStartsWithAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":23, + "l":[ + "2_b0ab1063bba4431f95710a8bd9c9e2bcd1cebc09fe6803cf87ee501d045e8ee0", + "2_9206a0a6b987410488c8020650788c597509af05aa5327f92cae1c999c401c34", + "2_d8f5c200e2c54b0f84f7df024576f799f484d0e258c6058142a4993daa5e2998" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"b5ba025e" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"c35929e3" + }, + "stringNotStartsWithDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":23, + "l":[ + "1_061a38ed8d5955d90b88d1b1949512a45032390a2581f3c7e36597f7459a48c7" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"72c4e1ac" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"2b16da78" + }, + "stringStartsWithAnyOfCleartextDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":30, + "l":[ + "u@", + "a@", + "b@" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"9e55f5cf" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"e170a185" + }, + "stringStartsWithAnyOfDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":22, + "l":[ + "2_52f47d1a193071a304e41812055cc1282d90287e5c993ae159fa08fb1ccd3656", + "2_6e5ae1bb586660088d330ac106bbf6b7f2b43be1ca70dbb766f203b76bc84049", + "2_3edba8b738135ad32e85d8695acb8d8511ac67d83f50da55abd9ba469da88efe" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"1d9b7603" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"dd5b3211" + }, + "stringStartsWithDogDefaultCat":{ + "t":1, + "r":[ + { + "c":[ + { + "u":{ + "a":"Email", + "c":22, + "l":[ + "1_55e0b0566d89dc0fda2323efcfb958c782a4648513bdcc4dc84b044fd34230dd" + ] + } + } + ], + "s":{ + "v":{ + "s":"Dog" + }, + "i":"3b409872" + } + } + ], + "v":{ + "s":"Cat" + }, + "i":"3659b0fe" + } + } + } + """.trimIndent() + override val tests: Array = arrayOf( + TestCase( + key = "boolTrueIn202304", + defaultValue = true, + returnValue = false, + user = ConfigCatUser("12345", custom = mapOf("Custom1" to "2023.04.10")), + expectedLog = """WARNING [3004] Cannot evaluate condition (User.Custom1 AFTER '1.6803072E9' (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 '1.6803072E9' (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'.""" + ) + ) +}