diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fdaa52d..56b0a58 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,9 +8,9 @@ semantic-version = "2.1.1" junit-jupiter = "5.10.0" logback = "1.5.19" mockwebserver = "4.12.0" -mockito = "4.8.0" +mockito = "5.12.0" android-retrofuture = "1.7.4" -android-gradle = "8.0.0" +android-gradle = "8.2.0" [libraries] slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j-api" } @@ -21,7 +21,7 @@ junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.re logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } logback-core = { module = "ch.qos.logback:logback-core", version.ref = "logback" } mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "mockwebserver" } -mockito-inline = { module = "org.mockito:mockito-inline", version.ref = "mockito" } +mockito-inline = { module = "org.mockito:mockito-core", version.ref = "mockito" } junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit-jupiter" } junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit-jupiter" } android-retrofuture = { module = "net.sourceforge.streamsupport:android-retrofuture", version.ref = "android-retrofuture" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 87b5be7..20dfef1 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Thu Aug 04 20:00:01 CEST 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/src/main/java/com/configcat/ConfigCatClient.java b/src/main/java/com/configcat/ConfigCatClient.java index 089a273..bb27b74 100644 --- a/src/main/java/com/configcat/ConfigCatClient.java +++ b/src/main/java/com/configcat/ConfigCatClient.java @@ -92,7 +92,7 @@ public T getValue(Class classOfT, String key, User user, T defaultValue) private static void validateReturnType(Class classOfT) { if (!(classOfT == String.class || classOfT == Integer.class || classOfT == int.class || classOfT == Double.class || classOfT == double.class || classOfT == Boolean.class || classOfT == boolean.class)) { - throw new IllegalArgumentException("Only String, Integer, Double or Boolean types are supported."); + throw new EvaluationException("Only String, Integer, Double or Boolean types are supported."); } } @@ -121,19 +121,16 @@ public EvaluationDetails getValueDetails(Class classOfT, String key, T public EvaluationDetails getValueDetails(Class classOfT, String key, User user, T defaultValue) { if (key == null || key.isEmpty()) throw new IllegalArgumentException("'key' cannot be null or empty."); - - validateReturnType(classOfT); - try { return this.getValueDetailsAsync(classOfT, key, user, defaultValue).get(); } catch (InterruptedException e) { String error = "Thread interrupted."; this.logger.error(0, error, e); Thread.currentThread().interrupt(); - return EvaluationDetails.fromError(key, defaultValue, error + ": " + e.getMessage(), user); + return EvaluationDetails.fromError(key, defaultValue, EvaluationErrorCode.UNEXPECTED_ERROR,error + ": " + e.getMessage(), e, user); } catch (Exception e) { this.logger.error(1002, ConfigCatLogMessages.getSettingEvaluationErrorWithDefaultValue("getValueDetails", key, "defaultValue", defaultValue), e); - return EvaluationDetails.fromError(key, defaultValue, e.getMessage(), user); + return EvaluationDetails.fromError(key, defaultValue, EvaluationErrorCode.UNEXPECTED_ERROR, e.getMessage(), e, user); } } @@ -147,19 +144,30 @@ public CompletableFuture> getValueDetailsAsync(Class if (key == null || key.isEmpty()) throw new IllegalArgumentException("'key' cannot be null or empty."); - validateReturnType(classOfT); + try { + validateReturnType(classOfT); - return this.getSettingsAsync() + return this.getSettingsAsync() .thenApply(settingsResult -> { - Result checkSettingResult = checkSettingAvailable(settingsResult, key, defaultValue); + Result checkSettingResult = checkSettingAvailable(settingsResult, key, defaultValue); if (checkSettingResult.error() != null) { - EvaluationDetails evaluationDetails = EvaluationDetails.fromError(key, defaultValue, checkSettingResult.error(), user); + EvaluationDetails evaluationDetails = EvaluationDetails.fromError(key, defaultValue, checkSettingResult.errorCode(), checkSettingResult.error(), null, user); this.hooks.invokeOnFlagEvaluated(evaluationDetails); return evaluationDetails.asTypeSpecific(); } return this.evaluate(classOfT, checkSettingResult.value(), key, user != null ? user : this.defaultUser, settingsResult.fetchTime(), settingsResult.settings()); }); + } catch (InvalidConfigModelException e) { + this.logger.error(1002, ConfigCatLogMessages.getSettingEvaluationErrorWithDefaultValue("getValueDetails", key, "defaultValue", defaultValue), e); + return CompletableFuture.completedFuture(EvaluationDetails.fromError(key, defaultValue, EvaluationErrorCode.INVALID_CONFIG_MODEL, e.getMessage(), null, user)); + } catch (EvaluationException e) { + this.logger.error(1002, ConfigCatLogMessages.getSettingEvaluationErrorWithDefaultValue("getValueDetails", key, "defaultValue", defaultValue), e); + return CompletableFuture.completedFuture(EvaluationDetails.fromError(key, defaultValue, EvaluationErrorCode.SETTING_VALUE_TYPE_MISMATCH, e.getMessage(),null, user)); + } catch (Exception e) { + this.logger.error(1002, ConfigCatLogMessages.getSettingEvaluationErrorWithDefaultValue("getValueDetails", key, "defaultValue", defaultValue), e); + return CompletableFuture.completedFuture(EvaluationDetails.fromError(key, defaultValue, EvaluationErrorCode.UNEXPECTED_ERROR, e.getMessage(), e, user)); + } } @Override @@ -331,14 +339,14 @@ public RefreshResult forceRefresh() { } catch (Exception e) { this.logger.error(1003, ConfigCatLogMessages.getForceRefreshError("forceRefresh"), e); } - return new RefreshResult(false, "An error occurred during the refresh."); + return new RefreshResult(false, "An error occurred during the refresh.", RefreshErrorCode.UNEXPECTED_ERROR, null); } @Override public CompletableFuture forceRefreshAsync() { if (configService == null) { return CompletableFuture.completedFuture(new RefreshResult(false, - "The ConfigCat SDK is in local-only mode. Calling .forceRefresh() has no effect.")); + "The ConfigCat SDK is in local-only mode. Calling .forceRefresh() has no effect.", RefreshErrorCode.LOCAL_ONLY_CLIENT, null)); } return configService.refresh(); @@ -447,15 +455,15 @@ private CompletableFuture getSettingsAsync() { private T getValueFromSettingsMap(Class classOfT, SettingResult settingResult, String key, User user, T defaultValue) { User userObject = user != null ? user : this.defaultUser; try { - Result checkSettingResult = checkSettingAvailable(settingResult, key, defaultValue); + Result checkSettingResult = checkSettingAvailable(settingResult, key, defaultValue); if (checkSettingResult.error() != null) { - this.hooks.invokeOnFlagEvaluated(EvaluationDetails.fromError(key, defaultValue, checkSettingResult.error(), user)); + this.hooks.invokeOnFlagEvaluated(EvaluationDetails.fromError(key, defaultValue, checkSettingResult.errorCode(), checkSettingResult.error(), null, user)); return defaultValue; } return this.evaluate(classOfT, checkSettingResult.value(), key, userObject, settingResult.fetchTime(), settingResult.settings()).getValue(); } catch (Exception | NoSuchMethodError e) { FormattableLogMessage error = ConfigCatLogMessages.getSettingEvaluationFailedForOtherReason(key, "defaultValue", defaultValue); - this.hooks.invokeOnFlagEvaluated(EvaluationDetails.fromError(key, defaultValue, error + " " + e.getMessage(), userObject)); + this.hooks.invokeOnFlagEvaluated(EvaluationDetails.fromError(key, defaultValue, EvaluationErrorCode.UNEXPECTED_ERROR, error + " " + e.getMessage(), e, userObject)); this.logger.error(2001, error, e); return defaultValue; } @@ -518,6 +526,8 @@ private EvaluationDetails evaluate(Class classOfT, Setting setting, St user, false, null, + EvaluationErrorCode.NONE, + null, fetchTime, evaluationResult.targetingRule, evaluationResult.percentageOption); @@ -537,7 +547,7 @@ else if ((classOfT == Double.class || classOfT == double.class) && settingValue. else if ((classOfT == Boolean.class || classOfT == boolean.class) && settingValue.getBooleanValue() != null && SettingType.BOOLEAN.equals(settingType)) return settingValue.getBooleanValue(); - throw new IllegalArgumentException("The type of a setting must match the type of the specified default value. " + throw new EvaluationException("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 {" + classOfT + "}. " + "Please use a default value which corresponds to the setting type {" + settingType + "}." + "Learn more: https://configcat.com/docs/sdk-reference/android/#setting-type-mapping"); @@ -553,7 +563,7 @@ else if (settingType == SettingType.INT) else if (settingType == SettingType.DOUBLE) return double.class; else - throw new IllegalArgumentException("Only String, Integer, Double or Boolean types are supported"); + throw new EvaluationException("Only String, Integer, Double or Boolean types are supported"); } private boolean checkSettingsAvailable(SettingResult settingResult, String emptyResult) { @@ -565,11 +575,11 @@ private boolean checkSettingsAvailable(SettingResult settingResult, String empty return true; } - private Result checkSettingAvailable(SettingResult settingResult, String key, T defaultValue) { + private Result checkSettingAvailable(SettingResult settingResult, String key, T defaultValue) { if (settingResult.isEmpty()) { FormattableLogMessage errorMessage = ConfigCatLogMessages.getConfigJsonIsNotPresentedWithDefaultValue(key, "defaultValue", defaultValue); this.logger.error(1000, errorMessage); - return Result.error(errorMessage, null); + return Result.error(errorMessage, null, EvaluationErrorCode.CONFIG_JSON_NOT_AVAILABLE, null); } Map settings = settingResult.settings(); @@ -577,10 +587,10 @@ private Result checkSettingAvailable(SettingResult settingResult, S if (setting == null) { FormattableLogMessage errorMessage = ConfigCatLogMessages.getSettingEvaluationFailedDueToMissingKey(key, "defaultValue", defaultValue, settings.keySet()); this.logger.error(1001, errorMessage); - return Result.error(errorMessage, null); + return Result.error(errorMessage, null, EvaluationErrorCode.SETTING_KEY_MISSING, null); } - return Result.success(setting); + return Result.success(setting, EvaluationErrorCode.NONE); } /** diff --git a/src/main/java/com/configcat/ConfigFetcher.java b/src/main/java/com/configcat/ConfigFetcher.java index a6bebbd..47972b1 100644 --- a/src/main/java/com/configcat/ConfigFetcher.java +++ b/src/main/java/com/configcat/ConfigFetcher.java @@ -22,6 +22,8 @@ public enum Status { private final Status status; private final Entry entry; private final Object error; + private final RefreshErrorCode errorCode; + private final Throwable errorException; private final boolean fetchTimeUpdatable; private final String cfRayId; @@ -49,26 +51,32 @@ public Object error() { return this.error; } + public RefreshErrorCode errorCode() {return this.errorCode;} + + public Throwable errorException() {return this.errorException;} + public String cfRayId() {return this.cfRayId;} - FetchResponse(Status status, Entry entry, Object error, boolean fetchTimeUpdatable, String cfRayId) { + FetchResponse(Status status, Entry entry, Object error, RefreshErrorCode errorCode,Throwable errorException, boolean fetchTimeUpdatable, String cfRayId) { this.status = status; this.entry = entry; this.error = error; + this.errorCode = errorCode; + this.errorException = errorException; this.fetchTimeUpdatable = fetchTimeUpdatable; this.cfRayId = cfRayId; } public static FetchResponse fetched(Entry entry, String cfRayId) { - return new FetchResponse(Status.FETCHED, entry == null ? Entry.EMPTY : entry, null, false, cfRayId); + return new FetchResponse(Status.FETCHED, entry == null ? Entry.EMPTY : entry, null, RefreshErrorCode.NONE, null, false, cfRayId); } public static FetchResponse notModified(String cfRayId) { - return new FetchResponse(Status.NOT_MODIFIED, Entry.EMPTY, null, true, cfRayId); + return new FetchResponse(Status.NOT_MODIFIED, Entry.EMPTY, null, RefreshErrorCode.NONE, null, true, cfRayId); } - public static FetchResponse failed(Object error, boolean fetchTimeUpdatable, String cfRayId) { - return new FetchResponse(Status.FAILED, Entry.EMPTY, error, fetchTimeUpdatable, cfRayId); + public static FetchResponse failed(Object error, RefreshErrorCode errorCode, Throwable errorException, boolean fetchTimeUpdatable, String cfRayId) { + return new FetchResponse(Status.FAILED, Entry.EMPTY, error,errorCode, errorException, fetchTimeUpdatable, cfRayId); } } @@ -190,9 +198,9 @@ private void callHTTP(String previousETag, CompletableFuture resu if (responseCode == 200) { String content = readBody(urlConnection.getInputStream()); String eTag = readHeaderValue(responseHeaders,"ETag"); - Result configResult = deserializeConfig(content, cfRayId); + Result configResult = deserializeConfig(content, cfRayId); if (configResult.error() != null) { - fetchResponse = FetchResponse.failed(configResult.error(), false, cfRayId); + fetchResponse = FetchResponse.failed(configResult.error(), RefreshErrorCode.INVALID_HTTP_RESPONSE_CONTENT, null, false, cfRayId); } else { logger.debug("Fetch was successful: new config fetched."); fetchResponse = FetchResponse.fetched(new Entry(configResult.value(), eTag, content, System.currentTimeMillis()), cfRayId); @@ -207,24 +215,24 @@ private void callHTTP(String previousETag, CompletableFuture resu } else if (responseCode == 403 || responseCode == 404) { FormattableLogMessage message = ConfigCatLogMessages.getFetchFailedDueToInvalidSDKKey(cfRayId); logger.error(1100, message); - fetchResponse = FetchResponse.failed(message, true, cfRayId); + fetchResponse = FetchResponse.failed(message, RefreshErrorCode.INVALID_SDK_KEY, null, true, cfRayId); } else { FormattableLogMessage message = ConfigCatLogMessages.getFetchFailedDueToUnexpectedHttpResponse(responseCode, urlConnection.getResponseMessage(), cfRayId); logger.error(1101, message); - fetchResponse = FetchResponse.failed(message, false, cfRayId); + fetchResponse = FetchResponse.failed(message, RefreshErrorCode.UNEXPECTED_HTTP_RESPONSE, null,false, cfRayId); } } catch (SocketTimeoutException e) { FormattableLogMessage message = ConfigCatLogMessages.getFetchFailedDueToRequestTimeout(httpOptions.getConnectTimeoutMillis(), httpOptions.getReadTimeoutMillis(), cfRayId); logger.error(1102, message, e); - fetchResponse = FetchResponse.failed(message, false, cfRayId); + fetchResponse = FetchResponse.failed(message, RefreshErrorCode.HTTP_REQUEST_TIMEOUT, e, false, cfRayId); } catch (Exception e) { FormattableLogMessage message = ConfigCatLogMessages.getFetchFailedDueToUnexpectedError(cfRayId); logger.error(1103, message, e); - fetchResponse = FetchResponse.failed(message + " " + e.getMessage(), false, cfRayId); + fetchResponse = FetchResponse.failed(message, RefreshErrorCode.HTTP_REQUEST_FAILURE, e, false, cfRayId); } finally { if(fetchResponse == null) { - fetchResponse = FetchResponse.failed(ConfigCatLogMessages.getFetchFailedDueToUnexpectedError(cfRayId), false, cfRayId); + fetchResponse = FetchResponse.failed(ConfigCatLogMessages.getFetchFailedDueToUnexpectedError(cfRayId), RefreshErrorCode.UNEXPECTED_ERROR, null, false, cfRayId); } result.complete(fetchResponse); if (urlConnection != null) { @@ -271,13 +279,13 @@ private String readBody(InputStream inputStream) throws IOException { return body.toString(); } - private Result deserializeConfig(String json, String cfRayId) { + private Result deserializeConfig(String json, String cfRayId) { try { - return Result.success(Utils.deserializeConfig(json)); + return Result.success(Utils.deserializeConfig(json), EvaluationErrorCode.NONE); } catch (Exception e) { FormattableLogMessage message = ConfigCatLogMessages.getFetchReceived200WithInvalidBodyError(cfRayId); this.logger.error(1105, message, e); - return Result.error(message, null); + return Result.error(message, null, EvaluationErrorCode.INVALID_CONFIG_MODEL, null); } } } diff --git a/src/main/java/com/configcat/ConfigService.java b/src/main/java/com/configcat/ConfigService.java index 6b0808e..b554ea3 100644 --- a/src/main/java/com/configcat/ConfigService.java +++ b/src/main/java/com/configcat/ConfigService.java @@ -42,7 +42,7 @@ class ConfigService implements Closeable { private ScheduledExecutorService pollScheduler; private String cachedEntryString = ""; private Entry cachedEntry = Entry.EMPTY; - private CompletableFuture> runningTask; + private CompletableFuture> runningTask; private final AtomicBoolean initialized = new AtomicBoolean(false); private final AtomicBoolean userIndicatedOffline; private final AtomicBoolean inForegroundAndHasNetwork; @@ -96,7 +96,7 @@ public ConfigService(String sdkKey, hooks.invokeOnClientReady(determineCacheState()); FormattableLogMessage message = ConfigCatLogMessages.getAutoPollMaxInitWaitTimeReached(autoPollingMode.getMaxInitWaitTimeSeconds()); logger.warn(4200, message); - completeRunningTask(Result.error(message, cachedEntry)); + completeRunningTask(Result.error(message, cachedEntry, RefreshErrorCode.CLIENT_INIT_TIMED_OUT, null)); } } finally { lock.unlock(); @@ -133,11 +133,15 @@ public CompletableFuture refresh() { if (isOffline()) { String offlineWarning = ConfigCatLogMessages.CONFIG_SERVICE_CANNOT_INITIATE_HTTP_CALLS_WARN; logger.warn(3200, offlineWarning); - return CompletableFuture.completedFuture(new RefreshResult(false, offlineWarning)); + return CompletableFuture.completedFuture(new RefreshResult(false, offlineWarning, RefreshErrorCode.OFFLINE_CLIENT, null)); } return fetchIfOlder(Constants.DISTANT_FUTURE, false) - .thenApply(entryResult -> new RefreshResult(entryResult.error() == null, entryResult.error())); + .thenApply(entryResult -> { + boolean isSucceed = entryResult.error() == null; + return new RefreshResult(isSucceed, entryResult.error(), + isSucceed ? RefreshErrorCode.NONE : entryResult.errorCode(), entryResult.errorException()); + }); } public void setOnline() { @@ -167,7 +171,7 @@ private void switchStateIfNeeded() { } } - private CompletableFuture> fetchIfOlder(long threshold, boolean preferCached) { + private CompletableFuture> fetchIfOlder(long threshold, boolean preferCached) { lock.lock(); try { Entry fromCache = readCache(); @@ -216,7 +220,7 @@ private void processResponse(FetchResponse response) { writeCache(cachedEntry); } completeRunningTask(response.isFailed() - ? Result.error(response.error(), cachedEntry) + ? Result.error(response.error(), cachedEntry, response.errorCode(), response.errorException()) : Result.success(cachedEntry)); } setInitialized(); @@ -225,7 +229,7 @@ private void processResponse(FetchResponse response) { } } - private void completeRunningTask(Result result) { + private void completeRunningTask(Result result) { runningTask.complete(result); runningTask = null; } diff --git a/src/main/java/com/configcat/EvaluationDetails.java b/src/main/java/com/configcat/EvaluationDetails.java index 3609e49..346e5cd 100644 --- a/src/main/java/com/configcat/EvaluationDetails.java +++ b/src/main/java/com/configcat/EvaluationDetails.java @@ -10,9 +10,11 @@ public class EvaluationDetails { private final User user; private final boolean isDefaultValue; private final Object error; + private final EvaluationErrorCode errorCode; private final long fetchTimeUnixMilliseconds; private final TargetingRule matchedTargetingRule; private final PercentageOption matchedPercentageOption; + private final Throwable errorException; public EvaluationDetails(T value, String key, @@ -20,6 +22,8 @@ public EvaluationDetails(T value, User user, boolean isDefaultValue, Object error, + EvaluationErrorCode errorCode, + Throwable errorException, long fetchTimeUnixMilliseconds, TargetingRule matchedTargetingRule, PercentageOption matchedPercentageOption) { @@ -29,17 +33,20 @@ public EvaluationDetails(T value, this.user = user; this.isDefaultValue = isDefaultValue; this.error = error; + this.errorCode = errorCode; + this.errorException = errorException; this.fetchTimeUnixMilliseconds = fetchTimeUnixMilliseconds; this.matchedTargetingRule = matchedTargetingRule; this.matchedPercentageOption = matchedPercentageOption; + } - static EvaluationDetails fromError(String key, T defaultValue, Object error, User user) { - return new EvaluationDetails<>(defaultValue, key, "", user, true, error, Constants.DISTANT_PAST, null, null); + static EvaluationDetails fromError(String key, T defaultValue, EvaluationErrorCode errorCode, Object error, Throwable errorException, User user) { + return new EvaluationDetails<>(defaultValue, key, "", user, true, error, errorCode, errorException, Constants.DISTANT_PAST, null, null); } EvaluationDetails asTypeSpecific() { - return new EvaluationDetails<>((TR) value, key, variationId, user, isDefaultValue, error, fetchTimeUnixMilliseconds, matchedTargetingRule, matchedPercentageOption); + return new EvaluationDetails<>((TR) value, key, variationId, user, isDefaultValue, error, errorCode, errorException, fetchTimeUnixMilliseconds, matchedTargetingRule, matchedPercentageOption); } /** @@ -87,6 +94,20 @@ public String getError() { return null; } + /** + * The error code of the evaluation result. If the evaluation was successful, this will be EvaluationErrorCode.NONE. + */ + public EvaluationErrorCode getErrorCode() { + return errorCode; + } + + /** + * The error exception object related to the error. If the evaluation was successful, this will be null. + */ + public Throwable getErrorException() { + return errorException; + } + /** * The last fetch time of the config.json in unix milliseconds format. */ diff --git a/src/main/java/com/configcat/EvaluationErrorCode.java b/src/main/java/com/configcat/EvaluationErrorCode.java new file mode 100644 index 0000000..ba593ca --- /dev/null +++ b/src/main/java/com/configcat/EvaluationErrorCode.java @@ -0,0 +1,45 @@ +package com.configcat; + +/** + * Specifies the possible evaluation error codes. + */ +public enum EvaluationErrorCode implements ErrorCode { + + /** An unexpected error occurred during the evaluation. */ + UNEXPECTED_ERROR(-1), + + /** No error occurred (the evaluation was successful). */ + NONE(0), + + /** + * The evaluation failed because of an error in the config model. + * (Most likely, invalid data was passed to the SDK via flag overrides.) + */ + INVALID_CONFIG_MODEL(1), + + /** + * The evaluation failed because of a type mismatch between the evaluated + * setting value and the specified default value. + */ + SETTING_VALUE_TYPE_MISMATCH(2), + + /** The evaluation failed because the config JSON was not available locally. */ + CONFIG_JSON_NOT_AVAILABLE(1000), + + /** + * The evaluation failed because the key of the evaluated setting was not found in + * the config JSON. + */ + SETTING_KEY_MISSING(1001); + + public final int code; + + EvaluationErrorCode(int code) { + this.code = code; + } + + @Override + public int code() { + return this.code; + } +} diff --git a/src/main/java/com/configcat/RefreshErrorCode.java b/src/main/java/com/configcat/RefreshErrorCode.java new file mode 100644 index 0000000..51a6b72 --- /dev/null +++ b/src/main/java/com/configcat/RefreshErrorCode.java @@ -0,0 +1,54 @@ +package com.configcat; + +/** + * Specifies the possible config data refresh error codes. + */ +public enum RefreshErrorCode implements ErrorCode { + + /** An unexpected error occurred during the refresh operation. */ + UNEXPECTED_ERROR(-1), + + /** No error occurred (the refresh operation was successful). */ + NONE(0), + + /** + * The refresh operation failed because the client is configured to use the `OverrideBehaviour.LocalOnly` + * override behavior, which prevents synchronization with the external cache and making HTTP requests. + */ + LOCAL_ONLY_CLIENT(1), + + /** The refresh operation failed because the client is in offline mode, it cannot initiate HTTP requests. */ + OFFLINE_CLIENT(3200), + + /** + * The refresh operation failed because a HTTP response indicating an + * invalid SDK Key was received (403 Forbidden or 404 Not Found). + */ + INVALID_SDK_KEY(1100), + + /** The refresh operation failed because an invalid HTTP response was received (unexpected HTTP status code). */ + UNEXPECTED_HTTP_RESPONSE(1101), + + /** The refresh operation failed because the HTTP request timed out. */ + HTTP_REQUEST_TIMEOUT(1102), + + /** The refresh operation failed because the HTTP request failed (most likely, due to a local network issue). */ + HTTP_REQUEST_FAILURE(1103), + + /** The refresh operation failed because an invalid HTTP response was received (200 OK with an invalid content). */ + INVALID_HTTP_RESPONSE_CONTENT(1105), + + /** Initialization of the SDK timed out. **/ + CLIENT_INIT_TIMED_OUT(4200); + + public final int code; + + RefreshErrorCode(int code) { + this.code = code; + } + + @Override + public int code() { + return this.code; + } +} diff --git a/src/main/java/com/configcat/RefreshResult.java b/src/main/java/com/configcat/RefreshResult.java index f382201..2c8f273 100644 --- a/src/main/java/com/configcat/RefreshResult.java +++ b/src/main/java/com/configcat/RefreshResult.java @@ -1,15 +1,21 @@ package com.configcat; +import java.util.Map; + /** * Represents the result of a forceRefresh() call. */ public class RefreshResult { private final boolean success; private final Object error; + private final RefreshErrorCode errorCode; + private final Throwable errorException; - RefreshResult(boolean success, Object error) { + RefreshResult(boolean success, Object error, RefreshErrorCode errorCode, Throwable errorException) { this.success = success; this.error = error; + this.errorCode = errorCode; + this.errorException = errorException; } public boolean isSuccess() { @@ -22,4 +28,12 @@ public String error() { } return null; } + + public RefreshErrorCode errorCode() { + return errorCode; + } + + public Throwable errorException() { + return errorException; + } } diff --git a/src/main/java/com/configcat/RolloutEvaluator.java b/src/main/java/com/configcat/RolloutEvaluator.java index 43eb83e..b2370ae 100644 --- a/src/main/java/com/configcat/RolloutEvaluator.java +++ b/src/main/java/com/configcat/RolloutEvaluator.java @@ -93,7 +93,7 @@ private EvaluationResult evaluateTargetingRules(Setting setting, EvaluationConte } if (rule.getPercentageOptions() == null || rule.getPercentageOptions().length == 0) { - throw new IllegalArgumentException("Targeting rule THEN part is missing or invalid."); + throw new InvalidConfigModelException( "Targeting rule THEN part is missing or invalid."); } evaluateLogger.increaseIndentLevel(); @@ -193,7 +193,7 @@ private boolean evaluateUserCondition(UserCondition userCondition, EvaluationCon } if (comparator == null) { - throw new IllegalArgumentException(COMPARISON_OPERATOR_IS_INVALID); + throw new InvalidConfigModelException( COMPARISON_OPERATOR_IS_INVALID); } switch (comparator) { case CONTAINS_ANY_OF: @@ -265,7 +265,7 @@ private boolean evaluateUserCondition(UserCondition userCondition, EvaluationCon String[] userAttributeAsStringArray = getUserAttributeAsStringArray(userCondition, context, comparisonAttribute, userAttributeValue); return evaluateArrayContains(userCondition, configSalt, contextSalt, userAttributeAsStringArray, negateArrayContains, hashedArrayContains); default: - throw new IllegalArgumentException(COMPARISON_OPERATOR_IS_INVALID); + throw new InvalidConfigModelException( COMPARISON_OPERATOR_IS_INVALID); } } @@ -399,21 +399,21 @@ private boolean evaluateHashedStartOrEndsWith(UserCondition userCondition, Strin for (String comparisonValueHashedStartsEnds : comparisonValues) { int indexOf = ensureComparisonValue(comparisonValueHashedStartsEnds).indexOf("_"); if (indexOf <= 0) { - throw new IllegalArgumentException(COMPARISON_VALUE_IS_MISSING_OR_INVALID); + throw new InvalidConfigModelException( COMPARISON_VALUE_IS_MISSING_OR_INVALID); } String comparedTextLength = comparisonValueHashedStartsEnds.substring(0, indexOf).trim(); int comparedTextLengthInt; try { comparedTextLengthInt = Integer.parseInt(comparedTextLength); } catch (NumberFormatException e) { - throw new IllegalArgumentException(COMPARISON_VALUE_IS_MISSING_OR_INVALID); + throw new InvalidConfigModelException( COMPARISON_VALUE_IS_MISSING_OR_INVALID); } if (userAttributeValueUTF8.length < comparedTextLengthInt) { continue; } String comparisonHashValue = comparisonValueHashedStartsEnds.substring(indexOf + 1); if (comparisonHashValue.isEmpty()) { - throw new IllegalArgumentException(COMPARISON_VALUE_IS_MISSING_OR_INVALID); + throw new InvalidConfigModelException( COMPARISON_VALUE_IS_MISSING_OR_INVALID); } byte[] userValueSubStringByteArray; if (UserComparator.HASHED_STARTS_WITH.equals(comparator) || UserComparator.HASHED_NOT_STARTS_WITH.equals(comparator)) { @@ -550,11 +550,11 @@ private boolean evaluateSegmentCondition(SegmentCondition segmentCondition, Eval } if (segment == null) { - throw new IllegalArgumentException("Segment reference is invalid."); + throw new InvalidConfigModelException( "Segment reference is invalid."); } String segmentName = segment.getName(); if (segmentName == null || segmentName.isEmpty()) { - throw new IllegalArgumentException("Segment name is missing."); + throw new InvalidConfigModelException( "Segment name is missing."); } evaluateLogger.logSegmentEvaluationStart(segmentName); @@ -564,7 +564,7 @@ private boolean evaluateSegmentCondition(SegmentCondition segmentCondition, Eval SegmentComparator segmentComparator = SegmentComparator.fromId(segmentCondition.getSegmentComparator()); if (segmentComparator == null) { - throw new IllegalArgumentException("Segment comparison operator is invalid."); + throw new InvalidConfigModelException( "Segment comparison operator is invalid."); } switch (segmentComparator) { case IS_IN_SEGMENT: @@ -574,7 +574,7 @@ private boolean evaluateSegmentCondition(SegmentCondition segmentCondition, Eval result = !segmentRulesResult; break; default: - throw new IllegalArgumentException("Segment comparison operator is invalid."); + throw new InvalidConfigModelException( "Segment comparison operator is invalid."); } evaluateLogger.logSegmentEvaluationResult(segmentCondition, segment, result, segmentRulesResult); @@ -593,7 +593,7 @@ private boolean evaluatePrerequisiteFlagCondition(PrerequisiteFlagCondition prer String prerequisiteFlagKey = prerequisiteFlagCondition.getPrerequisiteFlagKey(); Setting prerequisiteFlagSetting = context.getSettings().get(prerequisiteFlagKey); if (prerequisiteFlagKey == null || prerequisiteFlagKey.isEmpty() || prerequisiteFlagSetting == null) { - throw new IllegalArgumentException("Prerequisite flag key is missing or invalid."); + throw new InvalidConfigModelException( "Prerequisite flag key is missing or invalid."); } SettingType settingType = prerequisiteFlagSetting .getType(); @@ -601,7 +601,7 @@ private boolean evaluatePrerequisiteFlagCondition(PrerequisiteFlagCondition prer (settingType == SettingType.STRING && prerequisiteFlagCondition.getValue().getStringValue() == null) || (settingType == SettingType.INT && prerequisiteFlagCondition.getValue().getIntegerValue() == null) || (settingType == SettingType.DOUBLE && prerequisiteFlagCondition.getValue().getDoubleValue() == null)) { - throw new IllegalArgumentException("Type mismatch between comparison value '" + prerequisiteFlagCondition.getValue() + "' and prerequisite flag '" + prerequisiteFlagKey + "'."); + throw new InvalidConfigModelException( "Type mismatch between comparison value '" + prerequisiteFlagCondition.getValue() + "' and prerequisite flag '" + prerequisiteFlagKey + "'."); } List visitedKeys = context.getVisitedKeys(); @@ -611,7 +611,7 @@ private boolean evaluatePrerequisiteFlagCondition(PrerequisiteFlagCondition prer visitedKeys.add(context.getKey()); if (visitedKeys.contains(prerequisiteFlagKey)) { String dependencyCycle = EvaluateLogger.formatCircularDependencyList(visitedKeys, prerequisiteFlagKey); - throw new IllegalArgumentException("Circular dependency detected between the following depending flags: " + dependencyCycle + "."); + throw new InvalidConfigModelException( "Circular dependency detected between the following depending flags: " + dependencyCycle + "."); } evaluateLogger.logPrerequisiteFlagEvaluationStart(prerequisiteFlagKey); @@ -633,7 +633,7 @@ private boolean evaluatePrerequisiteFlagCondition(PrerequisiteFlagCondition prer boolean result; if (prerequisiteComparator == null) { - throw new IllegalArgumentException("Prerequisite Flag comparison operator is invalid."); + throw new InvalidConfigModelException( "Prerequisite Flag comparison operator is invalid."); } switch (prerequisiteComparator) { case EQUALS: @@ -643,7 +643,7 @@ private boolean evaluatePrerequisiteFlagCondition(PrerequisiteFlagCondition prer result = !conditionValue.equalsBasedOnSettingType(evaluateResult.value, prerequisiteFlagSetting.getType()); break; default: - throw new IllegalArgumentException("Prerequisite Flag comparison operator is invalid."); + throw new InvalidConfigModelException( "Prerequisite Flag comparison operator is invalid."); } evaluateLogger.logPrerequisiteFlagEvaluationResult(prerequisiteFlagCondition, evaluateResult.value, result); @@ -695,19 +695,19 @@ private EvaluationResult evaluatePercentageOptions(PercentageOption[] percentage } } - throw new IllegalArgumentException("Sum of percentage option percentages is less than 100."); + throw new InvalidConfigModelException( "Sum of percentage option percentages is less than 100."); } private static T ensureComparisonValue(T value) { if (value == null) { - throw new IllegalArgumentException(COMPARISON_VALUE_IS_MISSING_OR_INVALID); + throw new InvalidConfigModelException( COMPARISON_VALUE_IS_MISSING_OR_INVALID); } return value; } private static String ensureConfigSalt(String configSalt){ if(configSalt == null){ - throw new IllegalArgumentException("Config JSON salt is missing."); + throw new InvalidConfigModelException( "Config JSON salt is missing."); } return configSalt; } @@ -717,7 +717,7 @@ private void validateSettingValueType(SettingValue settingValue, SettingType set || (SettingType.INT.equals(settingType) && settingValue.getIntegerValue() == null ) || (SettingType.DOUBLE.equals(settingType) && settingValue.getDoubleValue() == null) || (SettingType.BOOLEAN.equals(settingType) && settingValue.getBooleanValue() == null)) { - throw new IllegalArgumentException("Setting value is not of the expected type " + settingType.name() + "."); + throw new InvalidConfigModelException( "Setting value is not of the expected type " + settingType.name() + "."); } } } @@ -728,3 +728,9 @@ public RolloutEvaluatorException(String message) { } } +class InvalidConfigModelException extends IllegalArgumentException { + public InvalidConfigModelException(String message) { + super(message); + } +} + diff --git a/src/main/java/com/configcat/Utils.java b/src/main/java/com/configcat/Utils.java index a118b0a..b36bbca 100644 --- a/src/main/java/com/configcat/Utils.java +++ b/src/main/java/com/configcat/Utils.java @@ -64,13 +64,24 @@ private Constants() { /* prevent from instantiation*/ } static final int SDK_KEY_SECTION_LENGTH = 22; } -final class Result { +/** + * Common interface for Result. + */ +interface ErrorCode { + int code(); +} + +final class Result { private final T value; private final Object error; + private final E errorCode; + private final Throwable errorException; - private Result(T value, Object error) { + private Result(T value, Object error, E errorCode, Throwable errorException) { this.value = value; this.error = error; + this.errorCode = errorCode; + this.errorException = errorException; } T value() { @@ -81,11 +92,26 @@ Object error() { return this.error; } - static Result error(Object error, T value) { - return new Result<>(value, error); + E errorCode() {return this.errorCode;} + + Throwable errorException() {return this.errorException;} + + static Result error(Object error, T value, E errorCode, Throwable errorException) { + return new Result<>(value, error, errorCode, errorException); + } + + static Result success(T value) { + return new Result<>(value, null, RefreshErrorCode.NONE, null); } - static Result success(T value) { - return new Result<>(value, null); + static Result success(T value, E errorCode) { + return new Result<>(value, null, errorCode, null); } } + +final class EvaluationException extends IllegalArgumentException { + EvaluationException(String message) { + super(message); + } +} + diff --git a/src/test/java/com/configcat/ConfigCatClientTest.java b/src/test/java/com/configcat/ConfigCatClientTest.java index aa8d54d..e1be02c 100644 --- a/src/test/java/com/configcat/ConfigCatClientTest.java +++ b/src/test/java/com/configcat/ConfigCatClientTest.java @@ -1,6 +1,7 @@ package com.configcat; import java9.util.concurrent.CompletableFuture; +import kotlin.jvm.internal.Ref; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import org.junit.jupiter.api.Test; @@ -194,9 +195,12 @@ void getConfigurationReturnsPreviousCachedOnTimeout() throws IOException { server.enqueue(new MockResponse().setResponseCode(200).setBody(TEST_JSON)); server.enqueue(new MockResponse().setResponseCode(200).setBody("delayed").setBodyDelay(3, TimeUnit.SECONDS)); - cl.forceRefresh(); + RefreshResult result = cl.forceRefresh(); + assertEquals(RefreshErrorCode.NONE, result.errorCode()); assertEquals("fakeValue", cl.getValue(String.class, "fakeKey", null)); - cl.forceRefresh(); + result = cl.forceRefresh(); + assertEquals(RefreshErrorCode.HTTP_REQUEST_TIMEOUT, result.errorCode()); + assertEquals("Read timed out", result.errorException().getMessage()); assertEquals("fakeValue", cl.getValue(String.class, "fakeKey", null)); server.close(); @@ -237,9 +241,12 @@ void getConfigurationReturnsPreviousCachedOnFailAsync() throws IOException, Exec server.enqueue(new MockResponse().setResponseCode(200).setBody(TEST_JSON)); server.enqueue(new MockResponse().setResponseCode(500)); - cl.forceRefresh(); + RefreshResult result = cl.forceRefresh(); + assertEquals(RefreshErrorCode.NONE, result.errorCode()); assertEquals("fakeValue", cl.getValueAsync(String.class, "fakeKey", null).get()); - cl.forceRefresh(); + result = cl.forceRefresh(); + assertEquals(RefreshErrorCode.UNEXPECTED_HTTP_RESPONSE, result.errorCode()); + assertNull(result.errorException()); assertEquals("fakeValue", cl.getValueAsync(String.class, "fakeKey", null).get()); server.close(); @@ -262,10 +269,14 @@ void getValueReturnsDefaultOnExceptionRepeatedly() throws IOException { server.enqueue(new MockResponse().setResponseCode(200).setBody(badJson)); server.enqueue(new MockResponse().setResponseCode(200).setBody(badJson).setBodyDelay(3, TimeUnit.SECONDS)); - cl.forceRefresh(); + RefreshResult result = cl.forceRefresh(); + assertEquals(RefreshErrorCode.INVALID_HTTP_RESPONSE_CONTENT, result.errorCode()); + assertNull(result.errorException()); assertSame(def, cl.getValue(String.class, "test", def)); - cl.forceRefresh(); + result = cl.forceRefresh(); + assertEquals(RefreshErrorCode.HTTP_REQUEST_TIMEOUT, result.errorCode()); + assertEquals("Read timed out", result.errorException().getMessage()); assertSame(def, cl.getValue(String.class, "test", def)); server.shutdown(); @@ -285,7 +296,9 @@ void forceRefreshWithTimeout() throws IOException { server.enqueue(new MockResponse().setResponseCode(200).setBody("test").setBodyDelay(3, TimeUnit.SECONDS)); - cl.forceRefresh(); + RefreshResult result = cl.forceRefresh(); + assertEquals(RefreshErrorCode.HTTP_REQUEST_TIMEOUT, result.errorCode()); + assertEquals("Read timed out", result.errorException().getMessage()); server.shutdown(); cl.close(); @@ -358,7 +371,9 @@ void testAutoPollRefreshFail() throws IOException { options.baseUrl(server.url("/").toString()); }); - cl.forceRefresh(); + RefreshResult result = cl.forceRefresh(); + assertEquals(RefreshErrorCode.UNEXPECTED_HTTP_RESPONSE, result.errorCode()); + assertNull(result.errorException()); assertEquals("", cl.getValue(String.class, "fakeKey", "")); server.close(); @@ -377,7 +392,9 @@ void testLazyRefreshFail() throws IOException { options.baseUrl(server.url("/").toString()); }); - cl.forceRefresh(); + RefreshResult result = cl.forceRefresh(); + assertEquals(RefreshErrorCode.UNEXPECTED_HTTP_RESPONSE, result.errorCode()); + assertNull(result.errorException()); assertEquals("", cl.getValue(String.class, "fakeKey", "")); server.close(); @@ -396,7 +413,9 @@ void testManualPollRefreshFail() throws IOException { options.baseUrl(server.url("/").toString()); }); - cl.forceRefresh(); + RefreshResult result = cl.forceRefresh(); + assertEquals(RefreshErrorCode.UNEXPECTED_HTTP_RESPONSE, result.errorCode()); + assertNull(result.errorException()); assertEquals("", cl.getValue(String.class, "fakeKey", "")); server.close(); @@ -478,19 +497,25 @@ void testOnlineOffline() throws IOException { assertFalse(cl.isOffline()); - cl.forceRefresh(); + RefreshResult result = cl.forceRefresh(); + assertEquals(RefreshErrorCode.NONE, result.errorCode()); + assertNull(result.errorException()); assertEquals(1, server.getRequestCount()); cl.setOffline(); assertTrue(cl.isOffline()); - cl.forceRefresh(); + result = cl.forceRefresh(); + assertEquals(RefreshErrorCode.OFFLINE_CLIENT, result.errorCode()); + assertNull(result.errorException()); assertEquals(1, server.getRequestCount()); cl.setOnline(); - cl.forceRefresh(); + result = cl.forceRefresh(); + assertEquals(RefreshErrorCode.NONE, result.errorCode()); + assertNull(result.errorException()); assertEquals(2, server.getRequestCount()); @@ -513,7 +538,10 @@ void testInitOffline() throws IOException { assertTrue(cl.isOffline()); - cl.forceRefresh(); + RefreshResult refreshResult = cl.forceRefresh(); + assertFalse(refreshResult.isSuccess()); + assertEquals(RefreshErrorCode.OFFLINE_CLIENT, refreshResult.errorCode()); + assertEquals("Client is in offline mode, it cannot initiate HTTP calls.", refreshResult.error()); assertEquals(0, server.getRequestCount()); @@ -746,6 +774,8 @@ void testOnFlagEvaluationError() throws IOException { options.baseUrl(server.url("/").toString()); options.hooks().addOnFlagEvaluated(details -> { assertEquals("", details.getValue()); + assertEquals(EvaluationErrorCode.CONFIG_JSON_NOT_AVAILABLE, details.getErrorCode()); + assertNull(details.getErrorException()); assertEquals("Config JSON is not present when evaluating setting 'key'. Returning the `defaultValue` parameter that you specified in your application: ''.", details.getError()); assertTrue(details.isDefaultValue()); called.set(true); @@ -822,6 +852,7 @@ void getAllValueDetails() throws IOException { assertTrue((boolean) element.getValue()); assertFalse(element.isDefaultValue()); assertNull(element.getError()); + assertEquals(EvaluationErrorCode.NONE, element.getErrorCode()); assertEquals("fakeId1", element.getVariationId()); //assert result 2 @@ -830,6 +861,7 @@ void getAllValueDetails() throws IOException { assertFalse((boolean) element.getValue()); assertFalse(element.isDefaultValue()); assertNull(element.getError()); + assertEquals(EvaluationErrorCode.NONE, element.getErrorCode()); assertEquals("fakeId2", element.getVariationId()); server.shutdown(); cl.close(); @@ -859,6 +891,7 @@ void getAllValueDetailsAsync() throws IOException, ExecutionException, Interrupt assertTrue((boolean) element.getValue()); assertFalse(element.isDefaultValue()); assertNull(element.getError()); + assertEquals(EvaluationErrorCode.NONE, element.getErrorCode()); assertEquals("fakeId1", element.getVariationId()); //assert result 2 @@ -867,6 +900,7 @@ void getAllValueDetailsAsync() throws IOException, ExecutionException, Interrupt assertFalse((boolean) element.getValue()); assertFalse(element.isDefaultValue()); assertNull(element.getError()); + assertEquals(EvaluationErrorCode.NONE, element.getErrorCode()); assertEquals("fakeId2", element.getVariationId()); server.shutdown(); cl.close(); @@ -970,6 +1004,28 @@ void testGetValueInvalidTypes(String settingKey, Class callType, Object defaultV cl.close(); } + @ParameterizedTest + @MethodSource("testGetValueInvalidTypesData") + void testGetValueDetailsInvalidTypes(String settingKey, Class callType, Object defaultValue) throws IOException { + MockWebServer server = new MockWebServer(); + server.start(); + + server.enqueue(new MockResponse().setResponseCode(200).setBody(TEST_JSON_TYPES)); + + ConfigCatClient cl = ConfigCatClient.get(Helpers.SDK_KEY, options -> { + options.pollingMode(PollingModes.lazyLoad()); + options.baseUrl(server.url("/").toString()); + }); + + EvaluationDetails result = cl.getValueDetails(callType, settingKey, defaultValue); + assertEquals(EvaluationErrorCode.SETTING_VALUE_TYPE_MISMATCH, result.getErrorCode()); + assertEquals("Only String, Integer, Double or Boolean types are supported.", result.getError()); + assertNull(result.getErrorException()); + + server.shutdown(); + cl.close(); + } + @Test void testSpecialCharactersWorks() throws IOException { ClassLoader classLoader = getClass().getClassLoader(); diff --git a/src/test/java/com/configcat/ConfigFetcherTest.java b/src/test/java/com/configcat/ConfigFetcherTest.java index c8cc04f..481f0a2 100644 --- a/src/test/java/com/configcat/ConfigFetcherTest.java +++ b/src/test/java/com/configcat/ConfigFetcherTest.java @@ -78,6 +78,9 @@ void fetchException() throws IOException, ExecutionException, InterruptedExcepti FetchResponse response = fetch.fetchAsync(null).get(); assertTrue(response.isFailed()); + assertEquals(RefreshErrorCode.HTTP_REQUEST_TIMEOUT, response.errorCode()); + assertEquals("Request timed out while trying to fetch config JSON. Timeout values: [connect: 10000ms, read: 1000ms]", response.error().toString()); + assertEquals("Read timed out", response.errorException().getMessage()); assertEquals(Entry.EMPTY, response.entry()); fetch.close(); @@ -135,6 +138,8 @@ void fetchSuccess() throws Exception { FetchResponse response = fetcher.fetchAsync(null).get(); assertTrue(response.isFetched()); + assertNull(response.error()); + assertEquals(RefreshErrorCode.NONE, response.errorCode()); assertEquals("fakeValue", response.entry().getConfig().getEntries().get("fakeKey").getSettingsValue().getStringValue()); fetcher.close(); @@ -161,8 +166,12 @@ void fetchEmpty(String body) throws Exception { FetchResponse response = fetcher.fetchAsync(null).get(); assertFalse(response.isFetched()); + assertTrue(response.isFailed()); + assertEquals(RefreshErrorCode.INVALID_HTTP_RESPONSE_CONTENT, response.errorCode()); + assertNull(response.errorException()); assertEquals("Fetching config JSON was successful but the HTTP response content was invalid.", response.error().toString()); + fetcher.close(); } @@ -200,6 +209,8 @@ void fetchedFail403ContainsCFRAY() throws Exception { FetchResponse response = fetcher.fetchAsync("fakeETag").get(); assertTrue(response.isFailed()); + assertEquals(RefreshErrorCode.INVALID_SDK_KEY, response.errorCode()); + assertNull(response.errorException()); assertTrue(response.error().toString().contains("(Ray ID: 12345)")); verify(mockLogger, times(1)).error(anyString(), eq(1100), eq(ConfigCatLogMessages.getFetchFailedDueToInvalidSDKKey("12345"))); @@ -226,7 +237,8 @@ void fetchedNotModified304ContainsCFRAY() throws Exception { FetchResponse response = fetcher.fetchAsync("fakeETag").get(); assertTrue(response.isNotModified()); - + assertNull(response.error()); + assertEquals(RefreshErrorCode.NONE, response.errorCode()); verify(mockLogger, times(1)).debug(anyString(), eq(0), eq(String.format("Fetch was successful: config not modified. %s", ConfigCatLogMessages.getCFRayIdPostFix("12345")))); fetcher.close(); @@ -252,7 +264,8 @@ void fetchedReceivedInvalidBodyContainsCFRAY() throws Exception { assertTrue(response.isFailed()); assertTrue(response.error().toString().contains("(Ray ID: 12345)")); - + assertEquals(RefreshErrorCode.INVALID_HTTP_RESPONSE_CONTENT, response.errorCode()); + assertNull(response.errorException()); verify(mockLogger, times(1)).error(anyString(), eq(1105), eq(ConfigCatLogMessages.getFetchReceived200WithInvalidBodyError("12345")), any(), any(Exception.class)); fetcher.close(); @@ -280,7 +293,8 @@ void fetchFailedDueToRequestTimeoutContainsCFRAY() throws Exception { assertTrue(response.isFailed()); assertTrue(response.error().toString().contains("Request timed out while trying to fetch config JSON.")); assertTrue(response.error().toString().contains("(Ray ID: timeout-ray-123)")); - + assertEquals(RefreshErrorCode.HTTP_REQUEST_TIMEOUT, response.errorCode()); + assertEquals("Read timed out", response.errorException().getMessage()); verify(mockLogger, times(1)).error(anyString(), eq(1102), eq(ConfigCatLogMessages.getFetchFailedDueToRequestTimeout(10000, 1000, "timeout-ray-123")), any(), any(Exception.class)); fetcher.close(); @@ -312,6 +326,8 @@ void fetchFailedDueToUnexpectedErrorContainsCFRAY() throws Exception { assertTrue(response.isFailed()); assertTrue(response.error().toString().contains("Unexpected error occurred while trying to fetch config JSON.")); assertTrue(response.error().toString().contains("(Ray ID: unexpected-ray-456)")); + assertEquals(RefreshErrorCode.HTTP_REQUEST_FAILURE, response.errorCode()); + assertEquals("Premature EOF", response.errorException().getMessage()); fetcher.close(); } diff --git a/src/test/java/com/configcat/ConfigV2EvaluationTest.java b/src/test/java/com/configcat/ConfigV2EvaluationTest.java index 63f59e6..b81a340 100644 --- a/src/test/java/com/configcat/ConfigV2EvaluationTest.java +++ b/src/test/java/com/configcat/ConfigV2EvaluationTest.java @@ -154,7 +154,7 @@ public void prerequisiteFlagCircularDependencyTest(String key, String dependency }); EvaluationDetails result = client.getValueDetails(String.class, key, null, null); - assertEquals("java.lang.IllegalArgumentException: Circular dependency detected between the following depending flags: " + dependencyCycle + ".", result.getError()); + assertEquals("com.configcat.InvalidConfigModelException: Circular dependency detected between the following depending flags: " + dependencyCycle + ".", result.getError()); client.close(); } diff --git a/src/test/java/com/configcat/VariationIdTests.java b/src/test/java/com/configcat/VariationIdTests.java index 0b2df91..4b0798c 100644 --- a/src/test/java/com/configcat/VariationIdTests.java +++ b/src/test/java/com/configcat/VariationIdTests.java @@ -42,6 +42,7 @@ void getVariationIdWorks() { server.enqueue(new MockResponse().setResponseCode(200).setBody(TEST_JSON)); EvaluationDetails valueDetails = client.getValueDetails(Boolean.class, "key1", null); assertEquals("fakeId1", valueDetails.getVariationId()); + assertEquals(EvaluationErrorCode.NONE, valueDetails.getErrorCode()); } @Test @@ -49,6 +50,7 @@ void getVariationIdAsyncWorks() throws ExecutionException, InterruptedException server.enqueue(new MockResponse().setResponseCode(200).setBody(TEST_JSON)); EvaluationDetails valueDetails = client.getValueDetailsAsync(Boolean.class, "key2", null).get(); assertEquals("fakeId2", valueDetails.getVariationId()); + assertEquals(EvaluationErrorCode.NONE, valueDetails.getErrorCode()); } @Test @@ -56,6 +58,7 @@ void getVariationIdNotFound() { server.enqueue(new MockResponse().setResponseCode(200).setBody(TEST_JSON)); EvaluationDetails valueDetails = client.getValueDetails(Boolean.class, "nonexisting", false); assertEquals("", valueDetails.getVariationId()); + assertEquals(EvaluationErrorCode.SETTING_KEY_MISSING, valueDetails.getErrorCode()); } @Test @@ -65,8 +68,11 @@ void getAllVariationIdsWorks() { List> allValueDetails = client.getAllValueDetails(null); assertEquals(3, allValueDetails.size()); assertEquals("fakeId1", allValueDetails.get(0).getVariationId()); + assertEquals(EvaluationErrorCode.NONE, allValueDetails.get(0).getErrorCode()); assertEquals("fakeId2", allValueDetails.get(1).getVariationId()); + assertEquals(EvaluationErrorCode.NONE, allValueDetails.get(1).getErrorCode()); assertEquals("fakeId3", allValueDetails.get(2).getVariationId()); + assertEquals(EvaluationErrorCode.NONE, allValueDetails.get(2).getErrorCode()); } @@ -85,8 +91,11 @@ void getAllVariationIdsAsyncWorks() throws ExecutionException, InterruptedExcept List> allValueDetails = client.getAllValueDetailsAsync(null).get(); assertEquals(3, allValueDetails.size()); assertEquals("fakeId1", allValueDetails.get(0).getVariationId()); + assertEquals(EvaluationErrorCode.NONE, allValueDetails.get(0).getErrorCode()); assertEquals("fakeId2", allValueDetails.get(1).getVariationId()); + assertEquals(EvaluationErrorCode.NONE, allValueDetails.get(1).getErrorCode()); assertEquals("fakeId3", allValueDetails.get(2).getVariationId()); + assertEquals(EvaluationErrorCode.NONE, allValueDetails.get(2).getErrorCode()); }