Repository: firebase/firebase-admin-java
Pull request #1108 (2025-06-25 11:57:45)

Implement custom signal support for server side remote config
Hey there! So you want to contribute to a Firebase SDK? 
Before you file this pull request, please read these guidelines:

### Discussion

  * Read the contribution guidelines (CONTRIBUTING.md).
  * If this has been discussed in an issue, make sure to link to the issue here. 
    If not, go file an issue about this **before creating a pull request** to discuss.

### Testing

  * Make sure all existing tests in the repository pass after your change.
  * If you fixed a bug or added a feature, add a new test to cover your code.

### API Changes

  * At this time we cannot accept changes that affect the public API.  If you'd like to help 
    us make Firebase APIs better, please propose your change in an issue so that we 
    can discuss it together.



Patch:
diff --git a/src/main/java/com/google/firebase/remoteconfig/AndCondition.java b/src/main/java/com/google/firebase/remoteconfig/AndCondition.java
index 3be1056..46e2e21 100644
--- a/src/main/java/com/google/firebase/remoteconfig/AndCondition.java
+++ b/src/main/java/com/google/firebase/remoteconfig/AndCondition.java
@@ -1,4 +1,3 @@
-
 /*
  * Copyright 2025 Google LLC
  *
@@ -61,4 +60,3 @@ final class AndCondition {
                                                  .collect(Collectors.toList()));    
   }
 }
-
diff --git a/src/main/java/com/google/firebase/remoteconfig/ConditionEvaluator.java b/src/main/java/com/google/firebase/remoteconfig/ConditionEvaluator.java
new file mode 100644
index 0000000..e333ca2
--- /dev/null
+++ b/src/main/java/com/google/firebase/remoteconfig/ConditionEvaluator.java
@@ -0,0 +1,274 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.firebase.remoteconfig;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.collect.ImmutableList;
+import com.google.firebase.internal.NonNull;
+import com.google.firebase.internal.Nullable;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiPredicate;
+import java.util.function.IntPredicate;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+import java.util.stream.Collectors;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+ 
+final class ConditionEvaluator {
+  private static final int MAX_CONDITION_RECURSION_DEPTH = 10;
+  private static final Logger logger = LoggerFactory.getLogger(ConditionEvaluator.class);
+
+  /**
+   * Evaluates server conditions and assigns a boolean value to each condition.
+   * 
+   * @param conditions List of conditions which are to be evaluated.
+   * @param context A map with additional metadata used during evaluation. 
+   * @return A map of condition to evaluated value.
+   */
+  @NonNull
+  Map<String, Boolean> evaluateConditions(
+      @NonNull List<ServerCondition> conditions,
+      @Nullable KeysAndValues context) {
+    checkNotNull(conditions, "List of conditions must not be null.");
+    checkArgument(!conditions.isEmpty(), "List of conditions must not be empty.");
+    KeysAndValues evaluationContext = context != null 
+        ? context 
+        : new KeysAndValues.Builder().build();
+
+    Map<String, Boolean> evaluatedConditions = conditions.stream()
+        .collect(Collectors.toMap(
+            ServerCondition::getName,
+            condition -> 
+                evaluateCondition(condition.getCondition(), evaluationContext, /* nestingLevel= */0)
+        ));
+
+    return evaluatedConditions;
+  }
+
+  private boolean evaluateCondition(OneOfCondition condition, KeysAndValues context,
+      int nestingLevel) {
+    if (nestingLevel > MAX_CONDITION_RECURSION_DEPTH) {
+      logger.warn("Maximum condition recursion depth exceeded.");
+      return false;
+    }
+
+    if (condition.getOrCondition() != null) {
+      return evaluateOrCondition(condition.getOrCondition(), context, nestingLevel + 1);
+    } else if (condition.getAndCondition() != null) {
+      return evaluateAndCondition(condition.getAndCondition(), context, nestingLevel + 1);
+    } else if (condition.isTrue() != null) {
+      return true;
+    } else if (condition.isFalse() != null) {
+      return false;
+    } else if (condition.getCustomSignal() != null) {
+      return evaluateCustomSignalCondition(condition.getCustomSignal(), context);
+    }
+    logger.atWarn().log("Received invalid condition for evaluation.");
+    return false;
+  }
+
+
+  private boolean evaluateOrCondition(OrCondition condition, KeysAndValues context,
+      int nestingLevel) {
+    return condition.getConditions().stream()
+        .anyMatch(subCondition -> evaluateCondition(subCondition, context, nestingLevel + 1));
+  }
+
+  private boolean evaluateAndCondition(AndCondition condition, KeysAndValues context,
+      int nestingLevel) {
+    return condition.getConditions().stream()
+        .allMatch(subCondition -> evaluateCondition(subCondition, context, nestingLevel + 1));
+  }
+
+  private boolean evaluateCustomSignalCondition(CustomSignalCondition condition,
+      KeysAndValues context) {
+    CustomSignalOperator customSignalOperator = condition.getCustomSignalOperator();
+    String customSignalKey = condition.getCustomSignalKey();
+    ImmutableList<String> targetCustomSignalValues = ImmutableList.copyOf(
+        condition.getTargetCustomSignalValues());
+
+    if (targetCustomSignalValues.isEmpty()) {
+      logger.warn(String.format(
+          "Values must be assigned to all custom signal fields. Operator:%s, Key:%s, Values:%s",
+          customSignalOperator, customSignalKey, targetCustomSignalValues));
+      return false;
+    }
+
+    String customSignalValue = context.get(customSignalKey);
+    if (customSignalValue == null) {
+      return false;
+    }
+
+    switch (customSignalOperator) {
+      // String operations.
+      case STRING_CONTAINS:
+        return compareStrings(targetCustomSignalValues, customSignalValue,
+            (customSignal, targetSignal) -> customSignal.contains(targetSignal));
+      case STRING_DOES_NOT_CONTAIN:
+        return !compareStrings(targetCustomSignalValues, customSignalValue,
+            (customSignal, targetSignal) -> customSignal.contains(targetSignal));
+      case STRING_EXACTLY_MATCHES:
+        return compareStrings(targetCustomSignalValues, customSignalValue,
+            (customSignal, targetSignal) -> customSignal.equals(targetSignal));
+      case STRING_CONTAINS_REGEX:
+        return compareStrings(targetCustomSignalValues, customSignalValue,
+            (customSignal, targetSignal) -> compareStringRegex(customSignal, targetSignal));
+
+      // Numeric operations.
+      case NUMERIC_LESS_THAN:
+        return compareNumbers(targetCustomSignalValues, customSignalValue,
+            (result) -> result < 0);
+      case NUMERIC_LESS_EQUAL:
+        return compareNumbers(targetCustomSignalValues, customSignalValue,
+            (result) -> result <= 0);
+      case NUMERIC_EQUAL:
+        return compareNumbers(targetCustomSignalValues, customSignalValue,
+            (result) -> result == 0);
+      case NUMERIC_NOT_EQUAL:
+        return compareNumbers(targetCustomSignalValues, customSignalValue,
+            (result) -> result != 0);
+      case NUMERIC_GREATER_THAN:
+        return compareNumbers(targetCustomSignalValues, customSignalValue,
+            (result) -> result > 0);
+      case NUMERIC_GREATER_EQUAL:
+        return compareNumbers(targetCustomSignalValues, customSignalValue,
+            (result) -> result >= 0);
+
+      // Semantic operations.
+      case SEMANTIC_VERSION_EQUAL:
+        return compareSemanticVersions(targetCustomSignalValues, customSignalValue,
+            (result) -> result == 0);
+      case SEMANTIC_VERSION_GREATER_EQUAL:
+        return compareSemanticVersions(targetCustomSignalValues, customSignalValue,
+            (result) -> result >= 0);
+      case SEMANTIC_VERSION_GREATER_THAN:
+        return compareSemanticVersions(targetCustomSignalValues, customSignalValue,
+            (result) -> result > 0);
+      case SEMANTIC_VERSION_LESS_EQUAL:
+        return compareSemanticVersions(targetCustomSignalValues, customSignalValue,
+            (result) -> result <= 0);
+      case SEMANTIC_VERSION_LESS_THAN:
+        return compareSemanticVersions(targetCustomSignalValues, customSignalValue,
+            (result) -> result < 0);
+      case SEMANTIC_VERSION_NOT_EQUAL:
+        return compareSemanticVersions(targetCustomSignalValues, customSignalValue,
+            (result) -> result != 0);
+      default:
+        return false;
+    }
+  }
+
+  private boolean compareStrings(ImmutableList<String> targetValues, String customSignal,
+                              BiPredicate<String, String> compareFunction) {
+    return targetValues.stream().anyMatch(targetValue -> 
+              compareFunction.test(customSignal, targetValue));
+  }
+
+  private boolean compareStringRegex(String customSignal, String targetSignal) {
+    try {
+      return Pattern.compile(targetSignal).matcher(customSignal).matches();
+    } catch (PatternSyntaxException e) {
+      return false;
+    }
+  }
+
+  private boolean compareNumbers(ImmutableList<String> targetValues, String customSignal,
+                            IntPredicate compareFunction) {
+    if (targetValues.size() != 1) {
+      logger.warn(String.format(
+          "Target values must contain 1 element for numeric operations. Target Value: %s",
+          targetValues));
+      return false;
+    }
+
+    try {
+      double customSignalDouble = Double.parseDouble(customSignal);
+      double targetValue = Double.parseDouble(targetValues.get(0));
+      int comparisonResult = Double.compare(customSignalDouble, targetValue);
+      return compareFunction.test(comparisonResult);
+    } catch (NumberFormatException e) {
+      logger.warn("Error parsing numeric values: customSignal=%s, targetValue=%s",
+          customSignal, targetValues.get(0), e);
+      return false;
+    }
+  }
+
+  private boolean compareSemanticVersions(ImmutableList<String> targetValues,
+                                      String customSignal,
+                                      IntPredicate compareFunction) {
+    if (targetValues.size() != 1) {
+      logger.warn(String.format("Target values must contain 1 element for semantic operation."));
+      return false;
+    }
+
+    String targetValueString = targetValues.get(0);
+    if (!validateSemanticVersion(targetValueString)
+        || !validateSemanticVersion(customSignal)) {
+      return false;
+    }
+
+    List<Integer> targetVersion = parseSemanticVersion(targetValueString);
+    List<Integer> customSignalVersion = parseSemanticVersion(customSignal);
+
+    int maxLength = 5;
+    if (targetVersion.size() > maxLength || customSignalVersion.size() > maxLength) {
+      logger.warn("Semantic version max length(%s) exceeded. Target: %s, Custom Signal: %s",
+          maxLength, targetValueString, customSignal);
+      return false;
+    }
+
+    int comparison = compareSemanticVersions(customSignalVersion, targetVersion);
+    return compareFunction.test(comparison);
+  }
+
+  private int compareSemanticVersions(List<Integer> version1, List<Integer> version2) {
+    int maxLength = Math.max(version1.size(), version2.size());
+    int version1Size = version1.size();
+    int version2Size = version2.size();
+
+    for (int i = 0; i < maxLength; i++) {
+      // Default to 0 if segment is missing
+      int v1 =  i < version1Size ? version1.get(i) : 0;
+      int v2 =  i < version2Size ? version2.get(i) : 0;
+
+      int comparison = Integer.compare(v1, v2);
+      if (comparison != 0) {
+        return comparison;
+      }
+    }
+    // Versions are equal
+    return 0;
+  }
+
+  private List<Integer> parseSemanticVersion(String versionString) {
+    return Arrays.stream(versionString.split("\\."))
+          .map(Integer::parseInt)
+          .collect(Collectors.toList());
+  }
+
+  private boolean validateSemanticVersion(String version) {
+    Pattern pattern = Pattern.compile("^[0-9]+(?:\\.[0-9]+){0,4}$");
+    return pattern.matcher(version).matches();
+  }
+}
diff --git a/src/main/java/com/google/firebase/remoteconfig/CustomSignalCondition.java b/src/main/java/com/google/firebase/remoteconfig/CustomSignalCondition.java
new file mode 100644
index 0000000..a8d96ef
--- /dev/null
+++ b/src/main/java/com/google/firebase/remoteconfig/CustomSignalCondition.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.firebase.remoteconfig;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.firebase.internal.NonNull;
+import com.google.firebase.remoteconfig.internal.ServerTemplateResponse.CustomSignalConditionResponse;
+
+import java.util.ArrayList;
+import java.util.List;
+
+final class CustomSignalCondition {
+  private final String customSignalKey;
+  private final CustomSignalOperator customSignalOperator;
+  private final ImmutableList<String> targetCustomSignalValues;
+
+  public CustomSignalCondition(
+      @NonNull String customSignalKey,
+      @NonNull CustomSignalOperator customSignalOperator,
+      @NonNull List<String> targetCustomSignalValues) {
+    checkArgument(
+        !Strings.isNullOrEmpty(customSignalKey), "Custom signal key must not be null or empty.");
+    checkNotNull(customSignalOperator);
+    checkNotNull(targetCustomSignalValues);
+    checkArgument(
+        !targetCustomSignalValues.isEmpty(), "Target custom signal values must not be empty.");
+    this.customSignalKey = customSignalKey.trim();
+    this.customSignalOperator = customSignalOperator;
+    this.targetCustomSignalValues = ImmutableList.copyOf(targetCustomSignalValues);
+  }
+
+  CustomSignalCondition(CustomSignalConditionResponse customSignalCondition) {
+    checkArgument(
+        !Strings.isNullOrEmpty(customSignalCondition.getKey()),
+        "Custom signal key must not be null or empty.");
+    checkArgument(
+        !customSignalCondition.getTargetValues().isEmpty(),
+        "Target custom signal values must not be empty.");
+    this.customSignalKey = customSignalCondition.getKey().trim();
+    List<String> targetCustomSignalValuesList = customSignalCondition.getTargetValues();
+    this.targetCustomSignalValues = ImmutableList.copyOf(targetCustomSignalValuesList);
+    switch (customSignalCondition.getOperator()) {
+      case "NUMERIC_EQUAL":
+        this.customSignalOperator = CustomSignalOperator.NUMERIC_EQUAL;
+        break;
+      case "NUMERIC_GREATER_EQUAL":
+        this.customSignalOperator = CustomSignalOperator.NUMERIC_GREATER_EQUAL;
+        break;
+      case "NUMERIC_GREATER_THAN":
+        this.customSignalOperator = CustomSignalOperator.NUMERIC_GREATER_THAN;
+        break;
+      case "NUMERIC_LESS_EQUAL":
+        this.customSignalOperator = CustomSignalOperator.NUMERIC_LESS_EQUAL;
+        break;
+      case "NUMERIC_LESS_THAN":
+        this.customSignalOperator = CustomSignalOperator.NUMERIC_LESS_THAN;
+        break;
+      case "NUMERIC_NOT_EQUAL":
+        this.customSignalOperator = CustomSignalOperator.NUMERIC_NOT_EQUAL;
+        break;
+      case "SEMANTIC_VERSION_EQUAL":
+        this.customSignalOperator = CustomSignalOperator.SEMANTIC_VERSION_EQUAL;
+        break;
+      case "SEMANTIC_VERSION_GREATER_EQUAL":
+        this.customSignalOperator = CustomSignalOperator.SEMANTIC_VERSION_GREATER_EQUAL;
+        break;
+      case "SEMANTIC_VERSION_GREATER_THAN":
+        this.customSignalOperator = CustomSignalOperator.SEMANTIC_VERSION_GREATER_THAN;
+        break;
+      case "SEMANTIC_VERSION_LESS_EQUAL":
+        this.customSignalOperator = CustomSignalOperator.SEMANTIC_VERSION_LESS_EQUAL;
+        break;
+      case "SEMANTIC_VERSION_LESS_THAN":
+        this.customSignalOperator = CustomSignalOperator.SEMANTIC_VERSION_LESS_THAN;
+        break;
+      case "SEMANTIC_VERSION_NOT_EQUAL":
+        this.customSignalOperator = CustomSignalOperator.SEMANTIC_VERSION_NOT_EQUAL;
+        break;
+      case "STRING_CONTAINS":
+        this.customSignalOperator = CustomSignalOperator.STRING_CONTAINS;
+        break;
+      case "STRING_CONTAINS_REGEX":
+        this.customSignalOperator = CustomSignalOperator.STRING_CONTAINS_REGEX;
+        break;
+      case "STRING_DOES_NOT_CONTAIN":
+        this.customSignalOperator = CustomSignalOperator.STRING_DOES_NOT_CONTAIN;
+        break;
+      case "STRING_EXACTLY_MATCHES":
+        this.customSignalOperator = CustomSignalOperator.STRING_EXACTLY_MATCHES;
+        break;
+      default:
+        this.customSignalOperator = CustomSignalOperator.UNSPECIFIED;
+    }
+    checkArgument(
+        this.customSignalOperator != CustomSignalOperator.UNSPECIFIED,
+        "Custom signal operator passed is invalid");
+  }
+
+  @NonNull
+  String getCustomSignalKey() {
+    return customSignalKey;
+  }
+
+  @NonNull
+  CustomSignalOperator getCustomSignalOperator() {
+    return customSignalOperator;
+  }
+
+  @NonNull
+  List<String> getTargetCustomSignalValues() {
+    return new ArrayList<>(targetCustomSignalValues);
+  }
+
+  CustomSignalConditionResponse toCustomConditonResponse() {
+    CustomSignalConditionResponse customSignalConditionResponse =
+        new CustomSignalConditionResponse();
+    customSignalConditionResponse.setKey(this.customSignalKey);
+    customSignalConditionResponse.setOperator(this.customSignalOperator.getOperator());
+    customSignalConditionResponse.setTargetValues(this.targetCustomSignalValues);
+    return customSignalConditionResponse;
+  }
+}
diff --git a/src/main/java/com/google/firebase/remoteconfig/CustomSignalOperator.java b/src/main/java/com/google/firebase/remoteconfig/CustomSignalOperator.java
new file mode 100644
index 0000000..0c0924d
--- /dev/null
+++ b/src/main/java/com/google/firebase/remoteconfig/CustomSignalOperator.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.firebase.remoteconfig;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.base.Strings;
+import com.google.firebase.internal.NonNull;
+
+enum CustomSignalOperator {
+  NUMERIC_EQUAL("NUMERIC_EQUAL"),
+  NUMERIC_GREATER_EQUAL("NUMERIC_GREATER_EQUAL"),
+  NUMERIC_GREATER_THAN("NUMERIC_GREATER_THAN"),
+  NUMERIC_LESS_EQUAL("NUMERIC_LESS_EQUAL"),
+  NUMERIC_LESS_THAN("NUMERIC_LESS_THAN"),
+  NUMERIC_NOT_EQUAL("NUMERIC_NOT_EQUAL"),
+  SEMANTIC_VERSION_EQUAL("SEMANTIC_VERSION_EQUAL"),
+  SEMANTIC_VERSION_GREATER_EQUAL("SEMANTIC_VERSION_GREATER_EQUAL"),
+  SEMANTIC_VERSION_GREATER_THAN("SEMANTIC_VERSION_GREATER_THAN"),
+  SEMANTIC_VERSION_LESS_EQUAL("SEMANTIC_VERSION_LESS_EQUAL"),
+  SEMANTIC_VERSION_LESS_THAN("SEMANTIC_VERSION_LESS_THAN"),
+  SEMANTIC_VERSION_NOT_EQUAL("SEMANTIC_VERSION_NOT_EQUAL"),
+  STRING_CONTAINS("STRING_CONTAINS"),
+  STRING_CONTAINS_REGEX("STRING_CONTAINS_REGEX"),
+  STRING_DOES_NOT_CONTAIN("STRING_DOES_NOT_CONTAIN"),
+  STRING_EXACTLY_MATCHES("STRING_EXACTLY_MATCHES"),
+  UNSPECIFIED("CUSTOM_SIGNAL_OPERATOR_UNSPECIFIED");
+
+  private final String operator;
+
+  CustomSignalOperator(@NonNull String operator) {
+    checkArgument(!Strings.isNullOrEmpty(operator), "Operator must not be null or empty.");
+    this.operator = operator;
+  }
+
+  @NonNull
+  String getOperator() {
+    return operator;
+  }
+}
diff --git a/src/main/java/com/google/firebase/remoteconfig/KeysAndValues.java b/src/main/java/com/google/firebase/remoteconfig/KeysAndValues.java
index 97064d9..47b159b 100644
--- a/src/main/java/com/google/firebase/remoteconfig/KeysAndValues.java
+++ b/src/main/java/com/google/firebase/remoteconfig/KeysAndValues.java
@@ -1,4 +1,3 @@
-
 /*
  * Copyright 2025 Google LLC
  *
diff --git a/src/main/java/com/google/firebase/remoteconfig/OneOfCondition.java b/src/main/java/com/google/firebase/remoteconfig/OneOfCondition.java
index 255cee5..7e352ec 100644
--- a/src/main/java/com/google/firebase/remoteconfig/OneOfCondition.java
+++ b/src/main/java/com/google/firebase/remoteconfig/OneOfCondition.java
@@ -26,6 +26,7 @@ import com.google.firebase.remoteconfig.internal.ServerTemplateResponse.OneOfCon
 class OneOfCondition {
   private OrCondition orCondition;
   private AndCondition andCondition;
+  private CustomSignalCondition customSignal;
   private String trueValue;
   private String falseValue;
 
@@ -36,6 +37,10 @@ class OneOfCondition {
     if (oneOfconditionResponse.getAndCondition() != null) {
       this.andCondition = new AndCondition(oneOfconditionResponse.getAndCondition());
     }
+    if (oneOfconditionResponse.getCustomSignalCondition() != null) {
+      this.customSignal = 
+          new CustomSignalCondition(oneOfconditionResponse.getCustomSignalCondition());
+    }
   }
 
   @VisibleForTesting
@@ -66,6 +71,11 @@ class OneOfCondition {
     return falseValue;
   }
 
+  @Nullable
+  CustomSignalCondition getCustomSignal() {
+    return customSignal;
+  }
+
   OneOfCondition setOrCondition(@NonNull OrCondition orCondition) {
     checkNotNull(orCondition, "`Or` condition cannot be set to null.");
     this.orCondition = orCondition;
@@ -78,6 +88,12 @@ class OneOfCondition {
     return this;
   }
 
+  OneOfCondition setCustomSignal(@NonNull CustomSignalCondition customSignal) {
+    checkNotNull(customSignal, "`Custom signal` condition cannot be set to null.");
+    this.customSignal = customSignal;
+    return this;
+  }
+
   OneOfCondition setTrue() {
     this.trueValue = "true";
     return this;
@@ -96,6 +112,9 @@ class OneOfCondition {
     if (this.orCondition != null) {
       oneOfConditionResponse.setOrCondition(this.orCondition.toOrConditionResponse());
     }
+    if (this.customSignal != null) {
+      oneOfConditionResponse.setCustomSignalCondition(this.customSignal.toCustomConditonResponse());
+    }
     return oneOfConditionResponse;
   }
 }
diff --git a/src/main/java/com/google/firebase/remoteconfig/OrCondition.java b/src/main/java/com/google/firebase/remoteconfig/OrCondition.java
index 4d258f2..2842ec1 100644
--- a/src/main/java/com/google/firebase/remoteconfig/OrCondition.java
+++ b/src/main/java/com/google/firebase/remoteconfig/OrCondition.java
@@ -1,4 +1,3 @@
-
 /*
  * Copyright 2025 Google LLC
  *
@@ -58,4 +57,3 @@ final class OrCondition {
                                                  .collect(Collectors.toList()));    
   }
 }
-
diff --git a/src/main/java/com/google/firebase/remoteconfig/ServerConfig.java b/src/main/java/com/google/firebase/remoteconfig/ServerConfig.java
index 9e06300..8540578 100644
--- a/src/main/java/com/google/firebase/remoteconfig/ServerConfig.java
+++ b/src/main/java/com/google/firebase/remoteconfig/ServerConfig.java
@@ -100,4 +100,3 @@ public final class ServerConfig {
     return new Value(ValueSource.STATIC);
   }
 }
-
diff --git a/src/main/java/com/google/firebase/remoteconfig/ServerTemplate.java b/src/main/java/com/google/firebase/remoteconfig/ServerTemplate.java
index f169927..26ab104 100644
--- a/src/main/java/com/google/firebase/remoteconfig/ServerTemplate.java
+++ b/src/main/java/com/google/firebase/remoteconfig/ServerTemplate.java
@@ -28,10 +28,18 @@ public interface ServerTemplate {
     ServerTemplate build();
   }
   /**
+  * Proccess the template data with a condition evaluator 
+  * based on the provided context. 
+  */
+  ServerConfig evaluate(KeysAndValues context) throws FirebaseRemoteConfigException;
+  /**
+  * Proccess the template data without context.
+  */
+  ServerConfig evaluate() throws FirebaseRemoteConfigException;
+  /**
   * Fetches and caches the current active version of the project.
   */
   ApiFuture<Void> load() throws FirebaseRemoteConfigException;
 
   String toJson();
 }
-
diff --git a/src/main/java/com/google/firebase/remoteconfig/ServerTemplateData.java b/src/main/java/com/google/firebase/remoteconfig/ServerTemplateData.java
index 3467485..59d51b5 100644
--- a/src/main/java/com/google/firebase/remoteconfig/ServerTemplateData.java
+++ b/src/main/java/com/google/firebase/remoteconfig/ServerTemplateData.java
@@ -1,4 +1,3 @@
-
 /*
  * Copyright 2025 Google LLC
  *
diff --git a/src/main/java/com/google/firebase/remoteconfig/ServerTemplateImpl.java b/src/main/java/com/google/firebase/remoteconfig/ServerTemplateImpl.java
index 81c71e8..c85fa16 100644
--- a/src/main/java/com/google/firebase/remoteconfig/ServerTemplateImpl.java
+++ b/src/main/java/com/google/firebase/remoteconfig/ServerTemplateImpl.java
@@ -1,4 +1,3 @@
-
 /*
  * Copyright 2025 Google LLC
  *
@@ -19,18 +18,27 @@ package com.google.firebase.remoteconfig;
 
 import com.google.api.core.ApiFuture;
 import com.google.api.core.ApiFutures;
+import com.google.common.collect.ImmutableMap;
+import com.google.firebase.ErrorCode;
+import com.google.firebase.remoteconfig.internal.TemplateResponse.ParameterValueResponse;
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
 
+import java.util.HashMap;
+import java.util.Map;
 import java.util.concurrent.atomic.AtomicReference;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 public final class ServerTemplateImpl implements ServerTemplate {
 
   private final KeysAndValues defaultConfig;
   private FirebaseRemoteConfigClient client;
   private ServerTemplateData cache;
   private final AtomicReference<String> cachedTemplate; // Added field for cached template
-  
+  private static final Logger logger = LoggerFactory.getLogger(ServerTemplate.class);
+
   public static class Builder implements ServerTemplate.Builder {
     private KeysAndValues defaultConfig;
     private String cachedTemplate;
@@ -69,6 +77,36 @@ public final class ServerTemplateImpl implements ServerTemplate {
     }
   }
 
+  @Override
+  public ServerConfig evaluate(KeysAndValues context) throws FirebaseRemoteConfigException {
+    if (this.cache == null) {
+      throw new FirebaseRemoteConfigException(ErrorCode.FAILED_PRECONDITION,
+          "No Remote Config Server template in cache. Call load() before calling evaluate().");
+    }
+
+    Map<String, Value> configValues = new HashMap<>();
+    ImmutableMap<String, String> defaultConfigValues = defaultConfig.keysAndValues;
+    // Initializes config Value objects with default values.
+    for (String configName : defaultConfigValues.keySet()) {
+      configValues.put(configName, new Value(ValueSource.DEFAULT,
+          defaultConfigValues.get(configName)));
+    }
+
+    ConditionEvaluator conditionEvaluator = new ConditionEvaluator();
+    ImmutableMap<String, Boolean> evaluatedCondition = ImmutableMap.copyOf(
+        conditionEvaluator.evaluateConditions(cache.getServerConditions(), context));
+    ImmutableMap<String, Parameter> parameters = ImmutableMap.copyOf(cache.getParameters());
+    mergeDerivedConfigValues(evaluatedCondition, parameters, configValues);
+
+    return new ServerConfig(configValues);
+  }
+
+  @Override
+  public ServerConfig evaluate() throws FirebaseRemoteConfigException {
+    KeysAndValues context = new KeysAndValues.Builder().build();
+    return evaluate(context);
+  }
+
   @Override
   public ApiFuture<Void> load() throws FirebaseRemoteConfigException {
     String serverTemplate = client.getServerTemplate();
@@ -91,5 +129,62 @@ public final class ServerTemplateImpl implements ServerTemplate {
     Gson gson = new GsonBuilder().setPrettyPrinting().create();
     return gson.toJson(this.cache);
   }
-}
 
+  private void mergeDerivedConfigValues(ImmutableMap<String, Boolean> evaluatedCondition,
+      ImmutableMap<String, Parameter> parameters, Map<String, Value> configValues) {
+    // Overlays config Value objects derived by evaluating the template.
+    for (String parameterName : parameters.keySet()) {
+      Parameter parameter = parameters.get(parameterName);
+      if (parameter == null) {
+        logger.warn(String.format("Parameter value is not assigned for %s", parameterName));
+        continue;
+      }
+
+      ImmutableMap<String, ParameterValue> conditionalValues = ImmutableMap.copyOf(
+          parameter.getConditionalValues());
+      ParameterValue derivedValue = null;
+
+      // Iterates in order over condition list. If there is a value associated
+      // with a condition, this checks if the condition is true.
+      for (String conditionName : evaluatedCondition.keySet()) {
+        boolean conditionEvaluation = evaluatedCondition.get(conditionName);
+        if (conditionalValues.containsKey(conditionName) && conditionEvaluation) {
+          derivedValue = conditionalValues.get(conditionName);
+          break;
+        }
+      }
+
+      if (derivedValue != null && derivedValue.toParameterValueResponse().isUseInAppDefault()) {
+        logger.warn(
+            String.format("Derived value found for %s but parameter is set to use in app default.",
+                parameterName));
+        continue;
+      }
+
+      if (derivedValue != null) {
+        String parameterValue = derivedValue.toParameterValueResponse().getValue();
+        Value value = new Value(ValueSource.REMOTE, parameterValue);
+        configValues.put(parameterName, value);
+        continue;
+      }
+
+      ParameterValue defaultValue = parameter.getDefaultValue();
+      if (defaultValue == null) {
+        logger.warn(String.format("Default parameter value for %s is not set.",
+            parameterName));
+        continue;
+      }
+
+      ParameterValueResponse defaultValueResponse = defaultValue.toParameterValueResponse();
+      if (defaultValueResponse != null && defaultValueResponse.isUseInAppDefault()) {
+        logger.info(String.format("Default value for %s is set to use in app default.",
+            parameterName));
+        continue;
+      }
+
+      String parameterDefaultValue = defaultValue.toParameterValueResponse().getValue();
+      Value value = new Value(ValueSource.REMOTE, parameterDefaultValue);
+      configValues.put(parameterName, value);
+    }
+  }
+}


Test patch:
diff --git a/src/test/java/com/google/firebase/remoteconfig/ConditionEvaluatorTest.java b/src/test/java/com/google/firebase/remoteconfig/ConditionEvaluatorTest.java
new file mode 100644
index 0000000..e2277c7
--- /dev/null
+++ b/src/test/java/com/google/firebase/remoteconfig/ConditionEvaluatorTest.java
@@ -0,0 +1,581 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.firebase.remoteconfig;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.Arrays;
+import java.util.Map;
+
+import org.junit.Test;
+
+public class ConditionEvaluatorTest {
+
+  private final ConditionEvaluator conditionEvaluator = new ConditionEvaluator();
+
+  @Test
+  public void testEvaluateConditionsEmptyOrConditionThrowsException() {
+    IllegalArgumentException error = assertThrows(IllegalArgumentException.class,
+        () -> createOneOfOrCondition(null));
+    assertEquals("List of conditions for OR operation must not be empty.", error.getMessage());
+  }
+
+  @Test
+  public void testEvaluateConditionsEmptyOrAndConditionThrowsException() {
+    IllegalArgumentException error = assertThrows(IllegalArgumentException.class,
+        () -> createOneOfAndCondition(null));
+    assertEquals("List of conditions for AND operation must not be empty.", error.getMessage());
+  }
+
+  @Test
+  public void testEvaluateConditionsOrAndTrueToTrue() {
+    OneOfCondition oneOfConditionTrue = createOneOfTrueCondition();
+    OneOfCondition oneOfConditionAnd = createOneOfAndCondition(oneOfConditionTrue);
+    OneOfCondition oneOfConditionOr = createOneOfOrCondition(oneOfConditionAnd);
+    ServerCondition condition = new ServerCondition("is_enabled", oneOfConditionOr);
+    KeysAndValues context = new KeysAndValues.Builder().build();
+
+    Map<String, Boolean> result = conditionEvaluator.evaluateConditions(Arrays.asList(condition),
+        context);
+
+    assertTrue(result.get("is_enabled"));
+  }
+
+  @Test
+  public void testEvaluateConditionsOrAndFalseToFalse() {
+    OneOfCondition oneOfConditionFalse = createOneOfFalseCondition();
+    OneOfCondition oneOfConditionAnd = createOneOfAndCondition(oneOfConditionFalse);
+    OneOfCondition oneOfConditionOr = createOneOfOrCondition(oneOfConditionAnd);
+    ServerCondition condition = new ServerCondition("is_enabled", oneOfConditionOr);
+    KeysAndValues context = new KeysAndValues.Builder().build();
+
+    Map<String, Boolean> result = conditionEvaluator.evaluateConditions(Arrays.asList(condition),
+        context);
+
+    assertFalse(result.get("is_enabled"));
+  }
+
+  @Test
+  public void testEvaluateConditionsNonOrTopConditionToTrue() {
+    OneOfCondition oneOfConditionTrue = createOneOfTrueCondition();
+    ServerCondition condition = new ServerCondition("is_enabled", oneOfConditionTrue);
+    KeysAndValues context = new KeysAndValues.Builder().build();
+
+    Map<String, Boolean> result = conditionEvaluator.evaluateConditions(Arrays.asList(condition),
+        context);
+
+    assertTrue(result.get("is_enabled"));
+  }
+
+  @Test
+  public void testEvaluateConditionsCustomSignalNumericLessThanToFalse() {
+    ServerCondition condition = createCustomSignalServerCondition(
+        CustomSignalOperator.NUMERIC_LESS_THAN, ImmutableList.of("-50.0"));
+    KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder();
+    contextBuilder.put("signal_key", "-50.0");
+
+    Map<String, Boolean> result = conditionEvaluator.evaluateConditions(Arrays.asList(condition),
+        contextBuilder.build());
+
+    assertFalse(result.get("signal_key"));
+  }
+
+  @Test
+  public void testEvaluateConditionsCustomSignalNumericLessThanToTrue() {
+    ServerCondition condition = createCustomSignalServerCondition(
+        CustomSignalOperator.NUMERIC_LESS_THAN, ImmutableList.of("-50.0"));
+    KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder();
+    contextBuilder.put("signal_key", "-50.01");
+
+    Map<String, Boolean> result = conditionEvaluator.evaluateConditions(Arrays.asList(condition),
+        contextBuilder.build());
+
+    assertTrue(result.get("signal_key"));
+  }
+
+  @Test
+  public void testEvaluateConditionsCustomSignalInvalidValueNumericOperationToFalse() {
+    ServerCondition condition = createCustomSignalServerCondition(
+        CustomSignalOperator.NUMERIC_LESS_THAN, ImmutableList.of("non-numeric"));
+    KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder();
+    contextBuilder.put("signal_key", "-50.01");
+
+    Map<String, Boolean> result = conditionEvaluator.evaluateConditions(Arrays.asList(condition),
+        contextBuilder.build());
+
+    assertFalse(result.get("signal_key"));
+  }
+
+  @Test
+  public void testEvaluateConditionsCustomSignalNumericLessEqualToFalse() {
+    ServerCondition condition = createCustomSignalServerCondition(
+        CustomSignalOperator.NUMERIC_LESS_EQUAL, ImmutableList.of("-50.0"));
+    KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder();
+    contextBuilder.put("signal_key", "-50.0");
+
+    Map<String, Boolean> result = conditionEvaluator.evaluateConditions(Arrays.asList(condition),
+        contextBuilder.build());
+
+    assertTrue(result.get("signal_key"));
+  }
+
+  @Test
+  public void testEvaluateConditionsCustomSignalNumericLessEqualToTrue() {
+    ServerCondition condition = createCustomSignalServerCondition(
+        CustomSignalOperator.NUMERIC_LESS_EQUAL, ImmutableList.of("-50.0"));
+    KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder();
+    contextBuilder.put("signal_key", "-49.9");
+
+    Map<String, Boolean> result = conditionEvaluator.evaluateConditions(Arrays.asList(condition),
+        contextBuilder.build());
+
+    assertFalse(result.get("signal_key"));
+  }
+
+  @Test
+  public void testEvaluateConditionsCustomSignalNumericEqualsToTrue() {
+    ServerCondition condition = createCustomSignalServerCondition(
+        CustomSignalOperator.NUMERIC_EQUAL, ImmutableList.of("50.0"));
+    KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder();
+    contextBuilder.put("signal_key", "50.0");
+
+    Map<String, Boolean> result = conditionEvaluator.evaluateConditions(Arrays.asList(condition),
+        contextBuilder.build());
+
+    assertTrue(result.get("signal_key"));
+  }
+
+  @Test
+  public void testEvaluateConditionsCustomSignalNumericEqualsToFalse() {
+    ServerCondition condition = createCustomSignalServerCondition(
+        CustomSignalOperator.NUMERIC_EQUAL, ImmutableList.of("50.0"));
+    KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder();
+    contextBuilder.put("signal_key", "50.000001");
+
+    Map<String, Boolean> result = conditionEvaluator.evaluateConditions(Arrays.asList(condition),
+        contextBuilder.build());
+
+    assertFalse(result.get("signal_key"));
+  }
+
+  @Test
+  public void testEvaluateConditionsCustomSignalNumericNotEqualsToTrue() {
+    ServerCondition condition = createCustomSignalServerCondition(
+        CustomSignalOperator.NUMERIC_NOT_EQUAL, ImmutableList.of("50.0"));
+    KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder();
+    contextBuilder.put("signal_key", "50.0");
+
+    Map<String, Boolean> result = conditionEvaluator.evaluateConditions(Arrays.asList(condition),
+        contextBuilder.build());
+
+    assertFalse(result.get("signal_key"));
+  }
+
+  @Test
+  public void testEvaluateConditionsCustomSignalNumericNotEqualsToFalse() {
+    ServerCondition condition = createCustomSignalServerCondition(
+        CustomSignalOperator.NUMERIC_NOT_EQUAL, ImmutableList.of("50.0"));
+    KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder();
+    contextBuilder.put("signal_key", "50.000001");
+
+    Map<String, Boolean> result = conditionEvaluator.evaluateConditions(Arrays.asList(condition),
+        contextBuilder.build());
+
+    assertTrue(result.get("signal_key"));
+  }
+
+  @Test
+  public void testEvaluateConditionsCustomSignalNumericGreaterEqualToFalse() {
+    ServerCondition condition = createCustomSignalServerCondition(
+        CustomSignalOperator.NUMERIC_GREATER_EQUAL, ImmutableList.of("-50.0"));
+    KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder();
+    contextBuilder.put("signal_key", "-50.0");
+
+    Map<String, Boolean> result = conditionEvaluator.evaluateConditions(Arrays.asList(condition),
+        contextBuilder.build());
+
+    assertTrue(result.get("signal_key"));
+  }
+
+  @Test
+  public void testEvaluateConditionsCustomSignalNumericGreaterEqualToTrue() {
+    ServerCondition condition = createCustomSignalServerCondition(
+        CustomSignalOperator.NUMERIC_GREATER_EQUAL, ImmutableList.of("-50.0"));
+    KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder();
+    contextBuilder.put("signal_key", "-50.01");
+
+    Map<String, Boolean> result = conditionEvaluator.evaluateConditions(Arrays.asList(condition),
+        contextBuilder.build());
+
+    assertFalse(result.get("signal_key"));
+  }
+
+  @Test
+  public void testEvaluateConditionsCustomSignalNumericgreaterThanToFalse() {
+    ServerCondition condition = createCustomSignalServerCondition(
+        CustomSignalOperator.NUMERIC_GREATER_THAN, ImmutableList.of("-50.0"));
+    KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder();
+    contextBuilder.put("signal_key", "-50.0");
+
+    Map<String, Boolean> result = conditionEvaluator.evaluateConditions(Arrays.asList(condition),
+        contextBuilder.build());
+
+    assertFalse(result.get("signal_key"));
+  }
+
+  @Test
+  public void testEvaluateConditionsCustomSignalNumericGreaterThanToTrue() {
+    ServerCondition condition = createCustomSignalServerCondition(
+        CustomSignalOperator.NUMERIC_GREATER_THAN, ImmutableList.of("-50.0"));
+    KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder();
+    contextBuilder.put("signal_key", "-49.09");
+
+    Map<String, Boolean> result = conditionEvaluator.evaluateConditions(Arrays.asList(condition),
+        contextBuilder.build());
+
+    assertTrue(result.get("signal_key"));
+  }
+
+  @Test
+  public void testEvaluateConditionsCustomSignalStringContainsToTrue() {
+    ServerCondition condition = createCustomSignalServerCondition(
+        CustomSignalOperator.STRING_CONTAINS, ImmutableList.of("One", "hundred"));
+    KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder();
+    contextBuilder.put("signal_key", "Two hundred");
+
+    Map<String, Boolean> result = conditionEvaluator.evaluateConditions(Arrays.asList(condition),
+        contextBuilder.build());
+
+    assertTrue(result.get("signal_key"));
+  }
+
+  @Test
+  public void testEvaluateConditionsCustomSignalStringContainsToFalse() {
+    ServerCondition condition = createCustomSignalServerCondition(
+        CustomSignalOperator.STRING_CONTAINS, ImmutableList.of("One", "hundred"));
+    KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder();
+    contextBuilder.put("signal_key", "Two hudred");
+
+    Map<String, Boolean> result = conditionEvaluator.evaluateConditions(Arrays.asList(condition),
+        contextBuilder.build());
+
+    assertFalse(result.get("signal_key"));
+  }
+
+  @Test
+  public void testEvaluateConditionsCustomSignalStringDoesNotContainToTrue() {
+    ServerCondition condition = createCustomSignalServerCondition(
+        CustomSignalOperator.STRING_DOES_NOT_CONTAIN, ImmutableList.of("One", "hundred"));
+    KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder();
+    contextBuilder.put("signal_key", "Two hudred");
+
+    Map<String, Boolean> result = conditionEvaluator.evaluateConditions(Arrays.asList(condition),
+        contextBuilder.build());
+
+    assertTrue(result.get("signal_key"));
+  }
+
+  @Test
+  public void testEvaluateConditionsCustomSignalStringDoesNotContainToFalse() {
+    ServerCondition condition = createCustomSignalServerCondition(
+        CustomSignalOperator.STRING_DOES_NOT_CONTAIN, ImmutableList.of("One", "hundred"));
+    KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder();
+    contextBuilder.put("signal_key", "Two hundred");
+
+    Map<String, Boolean> result = conditionEvaluator.evaluateConditions(Arrays.asList(condition),
+        contextBuilder.build());
+
+    assertFalse(result.get("signal_key"));
+  }
+
+  @Test
+  public void testEvaluateConditionsCustomSignalStringExactlyMatchesToTrue() {
+    ServerCondition condition = createCustomSignalServerCondition(
+        CustomSignalOperator.STRING_EXACTLY_MATCHES, ImmutableList.of("hundred"));
+    KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder();
+    contextBuilder.put("signal_key", "hundred");
+
+    Map<String, Boolean> result = conditionEvaluator.evaluateConditions(Arrays.asList(condition),
+        contextBuilder.build());
+
+    assertTrue(result.get("signal_key"));
+  }
+
+  @Test
+  public void testEvaluateConditionsCustomSignalStringExactlyMatchesToFalse() {
+    ServerCondition condition = createCustomSignalServerCondition(
+        CustomSignalOperator.STRING_EXACTLY_MATCHES, ImmutableList.of("hundred"));
+    KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder();
+    contextBuilder.put("signal_key", "Two hundred");
+
+    Map<String, Boolean> result = conditionEvaluator.evaluateConditions(Arrays.asList(condition),
+        contextBuilder.build());
+
+    assertFalse(result.get("signal_key"));
+  }
+
+  @Test
+  public void testEvaluateConditionsCustomSignalStringContainsRegexToTrue() {
+    ServerCondition condition = createCustomSignalServerCondition(
+        CustomSignalOperator.STRING_CONTAINS_REGEX, ImmutableList.of(".*hund.*"));
+    KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder();
+    contextBuilder.put("signal_key", "hundred");
+
+    Map<String, Boolean> result = conditionEvaluator.evaluateConditions(Arrays.asList(condition),
+        contextBuilder.build());
+
+    assertTrue(result.get("signal_key"));
+  }
+
+  @Test
+  public void testEvaluateConditionsCustomSignalStringContainsRegexToFalse() {
+    ServerCondition condition = createCustomSignalServerCondition(
+        CustomSignalOperator.STRING_CONTAINS_REGEX, ImmutableList.of("$hund.*"));
+    KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder();
+    contextBuilder.put("signal_key", "Two ahundred");
+
+    Map<String, Boolean> result = conditionEvaluator.evaluateConditions(Arrays.asList(condition),
+        contextBuilder.build());
+
+    assertFalse(result.get("signal_key"));
+  }
+
+  @Test
+  public void testEvaluateConditionsCustomSignalStringContainsInvalidRegexToFalse() {
+    ServerCondition condition = createCustomSignalServerCondition(
+        CustomSignalOperator.STRING_CONTAINS_REGEX, ImmutableList.of("abc)"));
+    KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder();
+    contextBuilder.put("signal_key", "Two ahundred");
+
+    Map<String, Boolean> result = conditionEvaluator.evaluateConditions(Arrays.asList(condition),
+        contextBuilder.build());
+
+    assertFalse(result.get("signal_key"));
+  }
+
+  @Test
+  public void testEvaluateConditionCustomSignalSemanticLessThanToTrue() {
+    ServerCondition condition = createCustomSignalServerCondition(
+        CustomSignalOperator.SEMANTIC_VERSION_LESS_THAN, ImmutableList.of("50.0.20"));
+    KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder();
+    contextBuilder.put("signal_key", "50.0.2.0.1");
+
+    Map<String, Boolean> result = conditionEvaluator.evaluateConditions(Arrays.asList(condition),
+        contextBuilder.build());
+
+    assertTrue(result.get("signal_key"));
+  }
+
+  @Test
+  public void testEvaluateConditionCustomSignalSemanticLessThanToFalse() {
+    ServerCondition condition = createCustomSignalServerCondition(
+        CustomSignalOperator.SEMANTIC_VERSION_LESS_THAN, ImmutableList.of("50.0.20"));
+    KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder();
+    contextBuilder.put("signal_key", "50.0.20.0.0");
+
+    Map<String, Boolean> result = conditionEvaluator.evaluateConditions(Arrays.asList(condition),
+        contextBuilder.build());
+
+    assertFalse(result.get("signal_key"));
+  }
+
+  @Test
+  public void testEvaluateConditionCustomSignalSemanticLessThanInvalidVersionToFalse() {
+    ServerCondition condition = createCustomSignalServerCondition(
+        CustomSignalOperator.SEMANTIC_VERSION_LESS_THAN, ImmutableList.of("50.0.-20"));
+    KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder();
+    contextBuilder.put("signal_key", "50.0.2.0.1");
+
+    Map<String, Boolean> result = conditionEvaluator.evaluateConditions(Arrays.asList(condition),
+        contextBuilder.build());
+
+    assertFalse(result.get("signal_key"));
+  }
+
+  @Test
+  public void testEvaluateConditionCustomSignalSemanticLessEqualToTrue() {
+    ServerCondition condition = createCustomSignalServerCondition(
+        CustomSignalOperator.SEMANTIC_VERSION_LESS_EQUAL, ImmutableList.of("50.0.20.0.0"));
+    KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder();
+    contextBuilder.put("signal_key", "50.0.20.0.0");
+
+    Map<String, Boolean> result = conditionEvaluator.evaluateConditions(Arrays.asList(condition),
+        contextBuilder.build());
+
+    assertTrue(result.get("signal_key"));
+  }
+
+  @Test
+  public void testEvaluateConditionCustomSignalSemanticLessEqualToFalse() {
+    ServerCondition condition = createCustomSignalServerCondition(
+        CustomSignalOperator.SEMANTIC_VERSION_LESS_EQUAL, ImmutableList.of("50.0.2"));
+    KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder();
+    contextBuilder.put("signal_key", "50.0.2.1.0");
+
+    Map<String, Boolean> result = conditionEvaluator.evaluateConditions(Arrays.asList(condition),
+        contextBuilder.build());
+
+    assertFalse(result.get("signal_key"));
+  }
+
+  @Test
+  public void testEvaluateConditionCustomSignalSemanticGreaterThanToTrue() {
+    ServerCondition condition = createCustomSignalServerCondition(
+        CustomSignalOperator.SEMANTIC_VERSION_GREATER_THAN, ImmutableList.of("50.0.2"));
+    KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder();
+    contextBuilder.put("signal_key", "50.0.20.0.1");
+
+    Map<String, Boolean> result = conditionEvaluator.evaluateConditions(Arrays.asList(condition),
+        contextBuilder.build());
+
+    assertTrue(result.get("signal_key"));
+  }
+
+  @Test
+  public void testEvaluateConditionCustomSignalSemanticGreaterThanToFalse() {
+    ServerCondition condition = createCustomSignalServerCondition(
+        CustomSignalOperator.SEMANTIC_VERSION_GREATER_THAN, ImmutableList.of("50.0.20"));
+    KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder();
+    contextBuilder.put("signal_key", "50.0.20.0.0");
+
+    Map<String, Boolean> result = conditionEvaluator.evaluateConditions(Arrays.asList(condition),
+        contextBuilder.build());
+
+    assertFalse(result.get("signal_key"));
+  }
+
+  @Test
+  public void testEvaluateConditionCustomSignalSemanticGreaterEqualToTrue() {
+    ServerCondition condition = createCustomSignalServerCondition(
+        CustomSignalOperator.SEMANTIC_VERSION_GREATER_EQUAL, ImmutableList.of("50.0.20"));
+    KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder();
+    contextBuilder.put("signal_key", "50.0.20.0.0");
+
+    Map<String, Boolean> result = conditionEvaluator.evaluateConditions(Arrays.asList(condition),
+        contextBuilder.build());
+
+    assertTrue(result.get("signal_key"));
+  }
+
+  @Test
+  public void testEvaluateConditionCustomSignalSemanticGreaterEqualToFalse() {
+    ServerCondition condition = createCustomSignalServerCondition(
+        CustomSignalOperator.SEMANTIC_VERSION_GREATER_EQUAL, ImmutableList.of("50.0.20.1"));
+    KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder();
+    contextBuilder.put("signal_key", "50.0.20.0.0");
+
+    Map<String, Boolean> result = conditionEvaluator.evaluateConditions(Arrays.asList(condition),
+        contextBuilder.build());
+
+    assertFalse(result.get("signal_key"));
+  }
+
+  @Test
+  public void testEvaluateConditionCustomSignalSemanticEqualToTrue() {
+    ServerCondition condition = createCustomSignalServerCondition(
+        CustomSignalOperator.SEMANTIC_VERSION_EQUAL, ImmutableList.of("50.0.20"));
+    KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder();
+    contextBuilder.put("signal_key", "50.0.20.0.0");
+
+    Map<String, Boolean> result = conditionEvaluator.evaluateConditions(Arrays.asList(condition),
+        contextBuilder.build());
+
+    assertTrue(result.get("signal_key"));
+  }
+
+  @Test
+  public void testEvaluateConditionCustomSignalSemanticEqualToFalse() {
+    ServerCondition condition = createCustomSignalServerCondition(
+        CustomSignalOperator.SEMANTIC_VERSION_EQUAL, ImmutableList.of("50.0.20.1"));
+    KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder();
+    contextBuilder.put("signal_key", "50.0.20.0.0");
+
+    Map<String, Boolean> result = conditionEvaluator.evaluateConditions(Arrays.asList(condition),
+        contextBuilder.build());
+
+    assertFalse(result.get("signal_key"));
+  }
+
+  @Test
+  public void testEvaluateConditionCustomSignalSemanticNotEqualToTrue() {
+    ServerCondition condition = createCustomSignalServerCondition(
+        CustomSignalOperator.SEMANTIC_VERSION_NOT_EQUAL, ImmutableList.of("50.0.20.1"));
+    KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder();
+    contextBuilder.put("signal_key", "50.0.20.0.0");
+
+    Map<String, Boolean> result = conditionEvaluator.evaluateConditions(Arrays.asList(condition),
+        contextBuilder.build());
+
+    assertTrue(result.get("signal_key"));
+  }
+
+  @Test
+  public void testEvaluateConditionCustomSignalSemanticNotEqualToFalse() {
+    ServerCondition condition = createCustomSignalServerCondition(
+        CustomSignalOperator.SEMANTIC_VERSION_NOT_EQUAL, ImmutableList.of("50.0.20"));
+    KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder();
+    contextBuilder.put("signal_key", "50.0.20.0.0");
+
+    Map<String, Boolean> result = conditionEvaluator.evaluateConditions(Arrays.asList(condition),
+        contextBuilder.build());
+
+    assertFalse(result.get("signal_key"));
+  }
+
+  private ServerCondition createCustomSignalServerCondition(
+      CustomSignalOperator operator,
+      ImmutableList<String> targetCustomSignalValues) {
+    CustomSignalCondition condition = new CustomSignalCondition("signal_key", operator,
+        targetCustomSignalValues);
+    OneOfCondition oneOfConditionCustomSignal = new OneOfCondition();
+    oneOfConditionCustomSignal.setCustomSignal(condition);
+    return new ServerCondition("signal_key", oneOfConditionCustomSignal);
+  }
+
+  private OneOfCondition createOneOfOrCondition(OneOfCondition condition) {
+    OrCondition orCondition = condition != null ? new OrCondition(ImmutableList.of(condition))
+        : new OrCondition(ImmutableList.of());
+    OneOfCondition oneOfCondition = new OneOfCondition();
+    oneOfCondition.setOrCondition(orCondition);
+    return oneOfCondition;
+  }
+
+  private OneOfCondition createOneOfAndCondition(OneOfCondition condition) {
+    AndCondition andCondition = condition != null ? new AndCondition(ImmutableList.of(condition))
+        : new AndCondition(ImmutableList.of());
+    OneOfCondition oneOfCondition = new OneOfCondition();
+    oneOfCondition.setAndCondition(andCondition);
+    return oneOfCondition;
+  }
+
+  private OneOfCondition createOneOfTrueCondition() {
+    OneOfCondition oneOfCondition = new OneOfCondition();
+    oneOfCondition.setTrue();
+    return oneOfCondition;
+  }
+
+  private OneOfCondition createOneOfFalseCondition() {
+    OneOfCondition oneOfCondition = new OneOfCondition();
+    oneOfCondition.setFalse();
+    return oneOfCondition;
+  }
+}
\ No newline at end of file
diff --git a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java
index aea2dd1..e925265 100644
--- a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java
+++ b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java
@@ -25,7 +25,6 @@ import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
-import com.google.api.client.googleapis.util.Utils;
 import com.google.api.client.http.GenericUrl;
 import com.google.api.client.http.HttpHeaders;
 import com.google.api.client.http.HttpMethods;
@@ -42,28 +41,30 @@ import com.google.firebase.ErrorCode;
 import com.google.firebase.FirebaseApp;
 import com.google.firebase.FirebaseOptions;
 import com.google.firebase.OutgoingHttpRequest;
-
 import com.google.firebase.auth.MockGoogleCredentials;
 import com.google.firebase.internal.ApiClientUtils;
 import com.google.firebase.internal.SdkUtils;
 import com.google.firebase.remoteconfig.internal.TemplateResponse;
 import com.google.firebase.testing.TestResponseInterceptor;
 import com.google.firebase.testing.TestUtils;
-
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.net.URLDecoder;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-
 import org.junit.Before;
 import org.junit.Test;
 
 public class FirebaseRemoteConfigClientImplTest {
 
   private static final String TEST_REMOTE_CONFIG_URL =
-          "https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/remoteConfig";
+      "https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/remoteConfig";
+  private static final String TEST_SERVER_REMOTE_CONFIG_URL =
+      "https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/namespaces/firebase-server/serverRemoteConfig";
 
   private static final List<Integer> HTTP_STATUS_CODES = ImmutableList.of(401, 404, 500);
 
@@ -74,57 +75,113 @@ public class FirebaseRemoteConfigClientImplTest {
 
   private static final String MOCK_TEMPLATE_RESPONSE = TestUtils
           .loadResource("getRemoteConfig.json");
+  
+  private static final String MOCK_SERVER_TEMPLATE_RESPONSE = TestUtils
+          .loadResource("getServerRemoteConfig.json");
 
   private static final String MOCK_LIST_VERSIONS_RESPONSE = TestUtils
           .loadResource("listRemoteConfigVersions.json");
 
   private static final String TEST_ETAG = "etag-123456789012-1";
 
-  private static final Map<String, Parameter> EXPECTED_PARAMETERS = ImmutableMap.of(
-          "welcome_message_text", new Parameter()
+  private static final Map<String, Parameter> EXPECTED_PARAMETERS =
+      ImmutableMap.of(
+          "welcome_message_text",
+              new Parameter()
                   .setDefaultValue(ParameterValue.of("welcome to app"))
-                  .setConditionalValues(ImmutableMap.<String, ParameterValue>of(
-                          "ios_en", ParameterValue.of("welcome to app en")
-                  ))
+                  .setConditionalValues(
+                      ImmutableMap.<String, ParameterValue>of(
+                          "ios_en", ParameterValue.of("welcome to app en")))
                   .setDescription("text for welcome message!")
                   .setValueType(ParameterValueType.STRING),
-          "header_text", new Parameter()
+          "header_text",
+              new Parameter()
                   .setDefaultValue(ParameterValue.inAppDefault())
-                  .setValueType(ParameterValueType.STRING)
-  );
-
-  private static final Map<String, ParameterGroup> EXPECTED_PARAMETER_GROUPS = ImmutableMap.of(
-          "new menu", new ParameterGroup()
-                  .setDescription("New Menu")
-                  .setParameters(ImmutableMap.of(
-                          "pumpkin_spice_season", new Parameter()
-                                  .setDefaultValue(ParameterValue.of("true"))
-                                  .setDescription("Whether it's currently pumpkin spice season.")
-                                  .setValueType(ParameterValueType.BOOLEAN)
-                          )
-                  )
-  );
-
-  private static final List<Condition> EXPECTED_CONDITIONS = ImmutableList.of(
+                  .setValueType(ParameterValueType.STRING));
+
+  private static final Map<String, ParameterGroup> EXPECTED_PARAMETER_GROUPS =
+      ImmutableMap.of(
+          "new menu",
+          new ParameterGroup()
+              .setDescription("New Menu")
+              .setParameters(
+                  ImmutableMap.of(
+                      "pumpkin_spice_season",
+                      new Parameter()
+                          .setDefaultValue(ParameterValue.of("true"))
+                          .setDescription("Whether it's currently pumpkin spice season.")
+                          .setValueType(ParameterValueType.BOOLEAN))));
+
+  private static final List<Condition> EXPECTED_CONDITIONS =
+      ImmutableList.of(
           new Condition("ios_en", "device.os == 'ios' && device.country in ['us', 'uk']")
-                  .setTagColor(TagColor.INDIGO),
-          new Condition("android_en",
-                  "device.os == 'android' && device.country in ['us', 'uk']")
-  );
-
-  private static final Version EXPECTED_VERSION = new Version(new TemplateResponse.VersionResponse()
-          .setVersionNumber("17")
-          .setUpdateOrigin("ADMIN_SDK_NODE")
-          .setUpdateType("INCREMENTAL_UPDATE")
-          .setUpdateUser(new TemplateResponse.UserResponse()
-                  .setEmail("firebase-user@account.com")
-                  .setName("dev-admin")
-                  .setImageUrl("http://image.jpg"))
-          .setUpdateTime("2020-11-15T06:57:26.342763941Z")
-          .setDescription("promo config")
-  );
-
-  private static final Template EXPECTED_TEMPLATE = new Template()
+              .setTagColor(TagColor.INDIGO),
+          new Condition("android_en", "device.os == 'android' && device.country in ['us', 'uk']"));
+
+  private static final List<ServerCondition> EXPECTED_SERVER_CONDITIONS =
+      ImmutableList.of(
+          new ServerCondition("custom_signal", null)
+              .setServerCondition(
+                  new OneOfCondition()
+                      .setOrCondition(
+                          new OrCondition(
+                              ImmutableList.of(
+                                  new OneOfCondition()
+                                      .setAndCondition(
+                                          new AndCondition(
+                                              ImmutableList.of(
+                                                  new OneOfCondition()
+                                                      .setCustomSignal(
+                                                          new CustomSignalCondition(
+                                                              "users",
+                                                              CustomSignalOperator
+                                                                  .NUMERIC_LESS_THAN,
+                                                              new ArrayList<>(
+                                                                  ImmutableList.of("100"))))))))))),
+          new ServerCondition("chained_conditions", null)
+              .setServerCondition(
+                  new OneOfCondition()
+                      .setOrCondition(
+                          new OrCondition(
+                              ImmutableList.of(
+                                  new OneOfCondition()
+                                      .setAndCondition(
+                                          new AndCondition(
+                                              ImmutableList.of(
+                                                  new OneOfCondition()
+                                                    .setCustomSignal(
+                                                      new CustomSignalCondition(
+                                                          "users",
+                                                        CustomSignalOperator
+                                                            .NUMERIC_LESS_THAN,
+                                                      new ArrayList<>(
+                                                        ImmutableList.of("100")))),
+                                                  new OneOfCondition()
+                                                    .setCustomSignal(
+                                                      new CustomSignalCondition(
+                                                        "premium users",
+                                                        CustomSignalOperator
+                                                            .NUMERIC_GREATER_THAN,
+                                                        new ArrayList<>(
+                                                            ImmutableList.of("20"))))
+                                                ))))))));
+
+  private static final Version EXPECTED_VERSION =
+      new Version(
+          new TemplateResponse.VersionResponse()
+              .setVersionNumber("17")
+              .setUpdateOrigin("ADMIN_SDK_NODE")
+              .setUpdateType("INCREMENTAL_UPDATE")
+              .setUpdateUser(
+                  new TemplateResponse.UserResponse()
+                      .setEmail("firebase-user@account.com")
+                      .setName("dev-admin")
+                      .setImageUrl("http://image.jpg"))
+              .setUpdateTime("2020-11-15T06:57:26.342763941Z")
+              .setDescription("promo config"));
+
+  private static final Template EXPECTED_TEMPLATE =
+      new Template()
           .setETag(TEST_ETAG)
           .setParameters(EXPECTED_PARAMETERS)
           .setConditions(EXPECTED_CONDITIONS)
@@ -159,16 +216,19 @@ public class FirebaseRemoteConfigClientImplTest {
 
   @Test
   public void testGetTemplateWithTimestampUpToNanosecondPrecision() throws Exception {
-    List<String> timestamps = ImmutableList.of(
+    List<String> timestamps =
+        ImmutableList.of(
             "2020-11-15T06:57:26.342Z",
             "2020-11-15T06:57:26.342763Z",
-            "2020-11-15T06:57:26.342763941Z"
-    );
+            "2020-11-15T06:57:26.342763941Z");
     for (String timestamp : timestamps) {
       response.addHeader("etag", TEST_ETAG);
-      String templateResponse = "{\"version\": {"
+      String templateResponse =
+          "{\"version\": {"
               + "    \"versionNumber\": \"17\","
-              + "    \"updateTime\": \"" + timestamp + "\""
+              + "    \"updateTime\": \""
+              + timestamp
+              + "\""
               + "  }}";
       response.setContent(templateResponse);
 
@@ -222,8 +282,12 @@ public class FirebaseRemoteConfigClientImplTest {
         client.getTemplate();
         fail("No error thrown for HTTP error");
       } catch (FirebaseRemoteConfigException error) {
-        checkExceptionFromHttpResponse(error, HTTP_STATUS_TO_ERROR_CODE.get(code), null,
-                "Unexpected HTTP response with status: " + code + "\n{}", HttpMethods.GET);
+        checkExceptionFromHttpResponse(
+            error,
+            HTTP_STATUS_TO_ERROR_CODE.get(code),
+            null,
+            "Unexpected HTTP response with status: " + code + "\n{}",
+            HttpMethods.GET);
       }
       checkGetRequestHeader(interceptor.getLastRequest());
     }
@@ -238,8 +302,8 @@ public class FirebaseRemoteConfigClientImplTest {
       fail("No error thrown for HTTP error");
     } catch (FirebaseRemoteConfigException error) {
       assertEquals(ErrorCode.UNKNOWN, error.getErrorCode());
-      assertEquals("Unknown error while making a remote service call: transport error",
-              error.getMessage());
+      assertEquals(
+          "Unknown error while making a remote service call: transport error", error.getMessage());
       assertTrue(error.getCause() instanceof IOException);
       assertNull(error.getHttpResponse());
       assertNull(error.getRemoteConfigErrorCode());
@@ -272,8 +336,12 @@ public class FirebaseRemoteConfigClientImplTest {
         client.getTemplate();
         fail("No error thrown for HTTP error");
       } catch (FirebaseRemoteConfigException error) {
-        checkExceptionFromHttpResponse(error, HTTP_STATUS_TO_ERROR_CODE.get(code), null,
-                "Unexpected HTTP response with status: " + code + "\nnull", HttpMethods.GET);
+        checkExceptionFromHttpResponse(
+            error,
+            HTTP_STATUS_TO_ERROR_CODE.get(code),
+            null,
+            "Unexpected HTTP response with status: " + code + "\nnull",
+            HttpMethods.GET);
       }
       checkGetRequestHeader(interceptor.getLastRequest());
     }
@@ -288,8 +356,12 @@ public class FirebaseRemoteConfigClientImplTest {
         client.getTemplate();
         fail("No error thrown for HTTP error");
       } catch (FirebaseRemoteConfigException error) {
-        checkExceptionFromHttpResponse(error, HTTP_STATUS_TO_ERROR_CODE.get(code), null,
-                "Unexpected HTTP response with status: " + code + "\nnot json", HttpMethods.GET);
+        checkExceptionFromHttpResponse(
+            error,
+            HTTP_STATUS_TO_ERROR_CODE.get(code),
+            null,
+            "Unexpected HTTP response with status: " + code + "\nnot json",
+            HttpMethods.GET);
       }
       checkGetRequestHeader(interceptor.getLastRequest());
     }
@@ -340,8 +412,9 @@ public class FirebaseRemoteConfigClientImplTest {
 
   @Test
   public void testGetTemplateAtVersionWithInvalidString() throws Exception {
-    List<String> invalidVersionStrings = ImmutableList
-            .of("", " ", "abc", "t123", "123t", "t123t", "12t3", "#$*&^", "-123", "+123", "123.4");
+    List<String> invalidVersionStrings =
+        ImmutableList.of(
+            "", " ", "abc", "t123", "123t", "t123t", "12t3", "#$*&^", "-123", "+123", "123.4");
 
     for (String version : invalidVersionStrings) {
       try {
@@ -408,8 +481,12 @@ public class FirebaseRemoteConfigClientImplTest {
         client.getTemplateAtVersion("24");
         fail("No error thrown for HTTP error");
       } catch (FirebaseRemoteConfigException error) {
-        checkExceptionFromHttpResponse(error, HTTP_STATUS_TO_ERROR_CODE.get(code), null,
-                "Unexpected HTTP response with status: " + code + "\n{}", HttpMethods.GET);
+        checkExceptionFromHttpResponse(
+            error,
+            HTTP_STATUS_TO_ERROR_CODE.get(code),
+            null,
+            "Unexpected HTTP response with status: " + code + "\n{}",
+            HttpMethods.GET);
       }
       checkGetRequestHeader(interceptor.getLastRequest(), "?versionNumber=24");
     }
@@ -424,8 +501,8 @@ public class FirebaseRemoteConfigClientImplTest {
       fail("No error thrown for HTTP error");
     } catch (FirebaseRemoteConfigException error) {
       assertEquals(ErrorCode.UNKNOWN, error.getErrorCode());
-      assertEquals("Unknown error while making a remote service call: transport error",
-              error.getMessage());
+      assertEquals(
+          "Unknown error while making a remote service call: transport error", error.getMessage());
       assertTrue(error.getCause() instanceof IOException);
       assertNull(error.getHttpResponse());
       assertNull(error.getRemoteConfigErrorCode());
@@ -458,8 +535,12 @@ public class FirebaseRemoteConfigClientImplTest {
         client.getTemplateAtVersion("24");
         fail("No error thrown for HTTP error");
       } catch (FirebaseRemoteConfigException error) {
-        checkExceptionFromHttpResponse(error, HTTP_STATUS_TO_ERROR_CODE.get(code), null,
-                "Unexpected HTTP response with status: " + code + "\nnull", HttpMethods.GET);
+        checkExceptionFromHttpResponse(
+            error,
+            HTTP_STATUS_TO_ERROR_CODE.get(code),
+            null,
+            "Unexpected HTTP response with status: " + code + "\nnull",
+            HttpMethods.GET);
       }
       checkGetRequestHeader(interceptor.getLastRequest(), "?versionNumber=24");
     }
@@ -474,8 +555,12 @@ public class FirebaseRemoteConfigClientImplTest {
         client.getTemplateAtVersion("24");
         fail("No error thrown for HTTP error");
       } catch (FirebaseRemoteConfigException error) {
-        checkExceptionFromHttpResponse(error, HTTP_STATUS_TO_ERROR_CODE.get(code), null,
-                "Unexpected HTTP response with status: " + code + "\nnot json", HttpMethods.GET);
+        checkExceptionFromHttpResponse(
+            error,
+            HTTP_STATUS_TO_ERROR_CODE.get(code),
+            null,
+            "Unexpected HTTP response with status: " + code + "\nnot json",
+            HttpMethods.GET);
       }
       checkGetRequestHeader(interceptor.getLastRequest(), "?versionNumber=24");
     }
@@ -484,15 +569,17 @@ public class FirebaseRemoteConfigClientImplTest {
   @Test
   public void testGetTemplateAtVersionErrorWithDetails() {
     for (int code : HTTP_STATUS_CODES) {
-      response.setStatusCode(code).setContent(
+      response
+          .setStatusCode(code)
+          .setContent(
               "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\"}}");
 
       try {
         client.getTemplateAtVersion("24");
         fail("No error thrown for HTTP error");
       } catch (FirebaseRemoteConfigException error) {
-        checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, null, "test error",
-                HttpMethods.GET);
+        checkExceptionFromHttpResponse(
+            error, ErrorCode.INVALID_ARGUMENT, null, "test error", HttpMethods.GET);
       }
       checkGetRequestHeader(interceptor.getLastRequest(), "?versionNumber=24");
     }
@@ -501,17 +588,22 @@ public class FirebaseRemoteConfigClientImplTest {
   @Test
   public void testGetTemplateAtVersionErrorWithRcError() {
     for (int code : HTTP_STATUS_CODES) {
-      response.setStatusCode(code).setContent(
+      response
+          .setStatusCode(code)
+          .setContent(
               "{\"error\": {\"status\": \"INVALID_ARGUMENT\", "
-                      + "\"message\": \"[INVALID_ARGUMENT]: test error\"}}");
+                  + "\"message\": \"[INVALID_ARGUMENT]: test error\"}}");
 
       try {
         client.getTemplateAtVersion("24");
         fail("No error thrown for HTTP error");
       } catch (FirebaseRemoteConfigException error) {
-        checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT,
-                RemoteConfigErrorCode.INVALID_ARGUMENT, "[INVALID_ARGUMENT]: test error",
-                HttpMethods.GET);
+        checkExceptionFromHttpResponse(
+            error,
+            ErrorCode.INVALID_ARGUMENT,
+            RemoteConfigErrorCode.INVALID_ARGUMENT,
+            "[INVALID_ARGUMENT]: test error",
+            HttpMethods.GET);
       }
       checkGetRequestHeader(interceptor.getLastRequest(), "?versionNumber=24");
     }
@@ -554,7 +646,8 @@ public class FirebaseRemoteConfigClientImplTest {
   public void testPublishTemplateWithValidTemplateAndValidateOnlyTrue() throws Exception {
     response.addHeader("etag", TEST_ETAG);
     response.setContent(MOCK_TEMPLATE_RESPONSE);
-    Template expectedTemplate = new Template()
+    Template expectedTemplate =
+        new Template()
             .setETag("etag-123456789012-45")
             .setParameters(EXPECTED_PARAMETERS)
             .setConditions(EXPECTED_CONDITIONS)
@@ -567,8 +660,8 @@ public class FirebaseRemoteConfigClientImplTest {
     assertNotEquals(TEST_ETAG, validatedTemplate.getETag());
     assertEquals("etag-123456789012-45", validatedTemplate.getETag());
     assertEquals(expectedTemplate, validatedTemplate);
-    checkPutRequestHeader(interceptor.getLastRequest(), "?validateOnly=true",
-            "etag-123456789012-45");
+    checkPutRequestHeader(
+        interceptor.getLastRequest(), "?validateOnly=true", "etag-123456789012-45");
   }
 
   @Test
@@ -612,8 +705,12 @@ public class FirebaseRemoteConfigClientImplTest {
         client.publishTemplate(new Template().setETag(TEST_ETAG), false, false);
         fail("No error thrown for HTTP error");
       } catch (FirebaseRemoteConfigException error) {
-        checkExceptionFromHttpResponse(error, HTTP_STATUS_TO_ERROR_CODE.get(code), null,
-                "Unexpected HTTP response with status: " + code + "\n{}", HttpMethods.PUT);
+        checkExceptionFromHttpResponse(
+            error,
+            HTTP_STATUS_TO_ERROR_CODE.get(code),
+            null,
+            "Unexpected HTTP response with status: " + code + "\n{}",
+            HttpMethods.PUT);
       }
       checkPutRequestHeader(interceptor.getLastRequest());
     }
@@ -628,8 +725,8 @@ public class FirebaseRemoteConfigClientImplTest {
       fail("No error thrown for HTTP error");
     } catch (FirebaseRemoteConfigException error) {
       assertEquals(ErrorCode.UNKNOWN, error.getErrorCode());
-      assertEquals("Unknown error while making a remote service call: transport error",
-              error.getMessage());
+      assertEquals(
+          "Unknown error while making a remote service call: transport error", error.getMessage());
       assertTrue(error.getCause() instanceof IOException);
       assertNull(error.getHttpResponse());
       assertNull(error.getRemoteConfigErrorCode());
@@ -662,8 +759,12 @@ public class FirebaseRemoteConfigClientImplTest {
         client.publishTemplate(new Template().setETag(TEST_ETAG), false, false);
         fail("No error thrown for HTTP error");
       } catch (FirebaseRemoteConfigException error) {
-        checkExceptionFromHttpResponse(error, HTTP_STATUS_TO_ERROR_CODE.get(code), null,
-                "Unexpected HTTP response with status: " + code + "\nnull", HttpMethods.PUT);
+        checkExceptionFromHttpResponse(
+            error,
+            HTTP_STATUS_TO_ERROR_CODE.get(code),
+            null,
+            "Unexpected HTTP response with status: " + code + "\nnull",
+            HttpMethods.PUT);
       }
       checkPutRequestHeader(interceptor.getLastRequest());
     }
@@ -678,8 +779,12 @@ public class FirebaseRemoteConfigClientImplTest {
         client.publishTemplate(new Template().setETag(TEST_ETAG), false, false);
         fail("No error thrown for HTTP error");
       } catch (FirebaseRemoteConfigException error) {
-        checkExceptionFromHttpResponse(error, HTTP_STATUS_TO_ERROR_CODE.get(code), null,
-                "Unexpected HTTP response with status: " + code + "\nnot json", HttpMethods.PUT);
+        checkExceptionFromHttpResponse(
+            error,
+            HTTP_STATUS_TO_ERROR_CODE.get(code),
+            null,
+            "Unexpected HTTP response with status: " + code + "\nnot json",
+            HttpMethods.PUT);
       }
       checkPutRequestHeader(interceptor.getLastRequest());
     }
@@ -688,15 +793,17 @@ public class FirebaseRemoteConfigClientImplTest {
   @Test
   public void testPublishTemplateErrorWithDetails() {
     for (int code : HTTP_STATUS_CODES) {
-      response.setStatusCode(code).setContent(
+      response
+          .setStatusCode(code)
+          .setContent(
               "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\"}}");
 
       try {
         client.publishTemplate(new Template().setETag(TEST_ETAG), false, false);
         fail("No error thrown for HTTP error");
       } catch (FirebaseRemoteConfigException error) {
-        checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, null, "test error",
-                HttpMethods.PUT);
+        checkExceptionFromHttpResponse(
+            error, ErrorCode.INVALID_ARGUMENT, null, "test error", HttpMethods.PUT);
       }
       checkPutRequestHeader(interceptor.getLastRequest());
     }
@@ -705,17 +812,22 @@ public class FirebaseRemoteConfigClientImplTest {
   @Test
   public void testPublishTemplateErrorWithRcError() {
     for (int code : HTTP_STATUS_CODES) {
-      response.setStatusCode(code).setContent(
+      response
+          .setStatusCode(code)
+          .setContent(
               "{\"error\": {\"status\": \"INVALID_ARGUMENT\", "
-                      + "\"message\": \"[INVALID_ARGUMENT]: test error\"}}");
+                  + "\"message\": \"[INVALID_ARGUMENT]: test error\"}}");
 
       try {
         client.publishTemplate(new Template().setETag(TEST_ETAG), false, false);
         fail("No error thrown for HTTP error");
       } catch (FirebaseRemoteConfigException error) {
-        checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT,
-                RemoteConfigErrorCode.INVALID_ARGUMENT, "[INVALID_ARGUMENT]: test error",
-                HttpMethods.PUT);
+        checkExceptionFromHttpResponse(
+            error,
+            ErrorCode.INVALID_ARGUMENT,
+            RemoteConfigErrorCode.INVALID_ARGUMENT,
+            "[INVALID_ARGUMENT]: test error",
+            HttpMethods.PUT);
       }
       checkPutRequestHeader(interceptor.getLastRequest());
     }
@@ -730,8 +842,9 @@ public class FirebaseRemoteConfigClientImplTest {
 
   @Test
   public void testRollbackWithInvalidString() throws Exception {
-    List<String> invalidVersionStrings = ImmutableList
-            .of("", " ", "abc", "t123", "123t", "t123t", "12t3", "#$*&^", "-123", "+123", "123.4");
+    List<String> invalidVersionStrings =
+        ImmutableList.of(
+            "", " ", "abc", "t123", "123t", "t123t", "12t3", "#$*&^", "-123", "+123", "123.4");
 
     for (String version : invalidVersionStrings) {
       try {
@@ -755,8 +868,8 @@ public class FirebaseRemoteConfigClientImplTest {
     assertEquals(EXPECTED_TEMPLATE, rolledBackTemplate);
     assertEquals(1605423446000L, rolledBackTemplate.getVersion().getUpdateTime());
     checkPostRequestHeader(interceptor.getLastRequest(), ":rollback");
-    checkRequestContent(interceptor.getLastRequest(),
-            ImmutableMap.<String, Object>of("versionNumber", "24"));
+    checkRequestContent(
+        interceptor.getLastRequest(), ImmutableMap.<String, Object>of("versionNumber", "24"));
   }
 
   @Test
@@ -772,8 +885,8 @@ public class FirebaseRemoteConfigClientImplTest {
     assertEquals(0, template.getParameterGroups().size());
     assertNull(template.getVersion());
     checkPostRequestHeader(interceptor.getLastRequest(), ":rollback");
-    checkRequestContent(interceptor.getLastRequest(),
-            ImmutableMap.<String, Object>of("versionNumber", "24"));
+    checkRequestContent(
+        interceptor.getLastRequest(), ImmutableMap.<String, Object>of("versionNumber", "24"));
   }
 
   @Test(expected = IllegalStateException.class)
@@ -802,12 +915,16 @@ public class FirebaseRemoteConfigClientImplTest {
         client.rollback("24");
         fail("No error thrown for HTTP error");
       } catch (FirebaseRemoteConfigException error) {
-        checkExceptionFromHttpResponse(error, HTTP_STATUS_TO_ERROR_CODE.get(code), null,
-                "Unexpected HTTP response with status: " + code + "\n{}", HttpMethods.POST);
+        checkExceptionFromHttpResponse(
+            error,
+            HTTP_STATUS_TO_ERROR_CODE.get(code),
+            null,
+            "Unexpected HTTP response with status: " + code + "\n{}",
+            HttpMethods.POST);
       }
       checkPostRequestHeader(interceptor.getLastRequest(), ":rollback");
-      checkRequestContent(interceptor.getLastRequest(),
-              ImmutableMap.<String, Object>of("versionNumber", "24"));
+      checkRequestContent(
+          interceptor.getLastRequest(), ImmutableMap.<String, Object>of("versionNumber", "24"));
     }
   }
 
@@ -820,8 +937,8 @@ public class FirebaseRemoteConfigClientImplTest {
       fail("No error thrown for HTTP error");
     } catch (FirebaseRemoteConfigException error) {
       assertEquals(ErrorCode.UNKNOWN, error.getErrorCode());
-      assertEquals("Unknown error while making a remote service call: transport error",
-              error.getMessage());
+      assertEquals(
+          "Unknown error while making a remote service call: transport error", error.getMessage());
       assertTrue(error.getCause() instanceof IOException);
       assertNull(error.getHttpResponse());
       assertNull(error.getRemoteConfigErrorCode());
@@ -843,8 +960,8 @@ public class FirebaseRemoteConfigClientImplTest {
       assertNull(error.getRemoteConfigErrorCode());
     }
     checkPostRequestHeader(interceptor.getLastRequest(), ":rollback");
-    checkRequestContent(interceptor.getLastRequest(),
-            ImmutableMap.<String, Object>of("versionNumber", "24"));
+    checkRequestContent(
+        interceptor.getLastRequest(), ImmutableMap.<String, Object>of("versionNumber", "24"));
   }
 
   @Test
@@ -856,12 +973,16 @@ public class FirebaseRemoteConfigClientImplTest {
         client.rollback("24");
         fail("No error thrown for HTTP error");
       } catch (FirebaseRemoteConfigException error) {
-        checkExceptionFromHttpResponse(error, HTTP_STATUS_TO_ERROR_CODE.get(code), null,
-                "Unexpected HTTP response with status: " + code + "\nnull", HttpMethods.POST);
+        checkExceptionFromHttpResponse(
+            error,
+            HTTP_STATUS_TO_ERROR_CODE.get(code),
+            null,
+            "Unexpected HTTP response with status: " + code + "\nnull",
+            HttpMethods.POST);
       }
       checkPostRequestHeader(interceptor.getLastRequest(), ":rollback");
-      checkRequestContent(interceptor.getLastRequest(),
-              ImmutableMap.<String, Object>of("versionNumber", "24"));
+      checkRequestContent(
+          interceptor.getLastRequest(), ImmutableMap.<String, Object>of("versionNumber", "24"));
     }
   }
 
@@ -874,52 +995,63 @@ public class FirebaseRemoteConfigClientImplTest {
         client.rollback("24");
         fail("No error thrown for HTTP error");
       } catch (FirebaseRemoteConfigException error) {
-        checkExceptionFromHttpResponse(error, HTTP_STATUS_TO_ERROR_CODE.get(code), null,
-                "Unexpected HTTP response with status: " + code + "\nnot json", HttpMethods.POST);
+        checkExceptionFromHttpResponse(
+            error,
+            HTTP_STATUS_TO_ERROR_CODE.get(code),
+            null,
+            "Unexpected HTTP response with status: " + code + "\nnot json",
+            HttpMethods.POST);
       }
       checkPostRequestHeader(interceptor.getLastRequest(), ":rollback");
-      checkRequestContent(interceptor.getLastRequest(),
-              ImmutableMap.<String, Object>of("versionNumber", "24"));
+      checkRequestContent(
+          interceptor.getLastRequest(), ImmutableMap.<String, Object>of("versionNumber", "24"));
     }
   }
 
   @Test
   public void testRollbackErrorWithDetails() throws IOException {
     for (int code : HTTP_STATUS_CODES) {
-      response.setStatusCode(code).setContent(
+      response
+          .setStatusCode(code)
+          .setContent(
               "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\"}}");
 
       try {
         client.rollback("24");
         fail("No error thrown for HTTP error");
       } catch (FirebaseRemoteConfigException error) {
-        checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, null, "test error",
-                HttpMethods.POST);
+        checkExceptionFromHttpResponse(
+            error, ErrorCode.INVALID_ARGUMENT, null, "test error", HttpMethods.POST);
       }
       checkPostRequestHeader(interceptor.getLastRequest(), ":rollback");
-      checkRequestContent(interceptor.getLastRequest(),
-              ImmutableMap.<String, Object>of("versionNumber", "24"));
+      checkRequestContent(
+          interceptor.getLastRequest(), ImmutableMap.<String, Object>of("versionNumber", "24"));
     }
   }
 
   @Test
   public void testRollbackErrorWithRcError() throws IOException {
     for (int code : HTTP_STATUS_CODES) {
-      response.setStatusCode(code).setContent(
+      response
+          .setStatusCode(code)
+          .setContent(
               "{\"error\": {\"status\": \"INVALID_ARGUMENT\", "
-                      + "\"message\": \"[INVALID_ARGUMENT]: test error\"}}");
+                  + "\"message\": \"[INVALID_ARGUMENT]: test error\"}}");
 
       try {
         client.rollback("24");
         fail("No error thrown for HTTP error");
       } catch (FirebaseRemoteConfigException error) {
-        checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT,
-                RemoteConfigErrorCode.INVALID_ARGUMENT, "[INVALID_ARGUMENT]: test error",
-                HttpMethods.POST);
+        checkExceptionFromHttpResponse(
+            error,
+            ErrorCode.INVALID_ARGUMENT,
+            RemoteConfigErrorCode.INVALID_ARGUMENT,
+            "[INVALID_ARGUMENT]: test error",
+            HttpMethods.POST);
       }
       checkPostRequestHeader(interceptor.getLastRequest(), ":rollback");
-      checkRequestContent(interceptor.getLastRequest(),
-              ImmutableMap.<String, Object>of("versionNumber", "24"));
+      checkRequestContent(
+          interceptor.getLastRequest(), ImmutableMap.<String, Object>of("versionNumber", "24"));
     }
   }
 
@@ -941,33 +1073,36 @@ public class FirebaseRemoteConfigClientImplTest {
   public void testListVersionsWithOptions() throws Exception {
     response.setContent(MOCK_LIST_VERSIONS_RESPONSE);
 
-    TemplateResponse.ListVersionsResponse versionsList = client.listVersions(
+    TemplateResponse.ListVersionsResponse versionsList =
+        client.listVersions(
             ListVersionsOptions.builder()
-                    .setPageSize(10)
-                    .setPageToken("token")
-                    .setStartTimeMillis(1605219122000L)
-                    .setEndTimeMillis(1606245035000L)
-                    .setEndVersionNumber("29").build());
+                .setPageSize(10)
+                .setPageToken("token")
+                .setStartTimeMillis(1605219122000L)
+                .setEndTimeMillis(1606245035000L)
+                .setEndVersionNumber("29")
+                .build());
 
     assertTrue(versionsList.hasVersions());
 
     HttpRequest request = interceptor.getLastRequest();
-    String urlWithoutParameters = request.getUrl().toString()
-            .substring(0, request.getUrl().toString().lastIndexOf('?'));
-    final Map<String, String> expectedQuery = ImmutableMap.of(
+    String urlWithoutParameters =
+        request.getUrl().toString().substring(0, request.getUrl().toString().lastIndexOf('?'));
+    final Map<String, String> expectedQuery =
+        ImmutableMap.of(
             "endVersionNumber", "29",
             "pageSize", "10",
             "pageToken", "token",
             "startTime", "2020-11-12T22:12:02.000000000Z",
-            "endTime", "2020-11-24T19:10:35.000000000Z"
-    );
+            "endTime", "2020-11-24T19:10:35.000000000Z");
     Map<String, String> actualQuery = new HashMap<>();
     String query = request.getUrl().toURI().getQuery();
     String[] pairs = query.split("&");
     for (String pair : pairs) {
       int idx = pair.indexOf("=");
-      actualQuery.put(URLDecoder.decode(pair.substring(0, idx), "UTF-8"),
-              URLDecoder.decode(pair.substring(idx + 1), "UTF-8"));
+      actualQuery.put(
+          URLDecoder.decode(pair.substring(0, idx), "UTF-8"),
+          URLDecoder.decode(pair.substring(idx + 1), "UTF-8"));
     }
 
     assertEquals("GET", request.getRequestMethod());
@@ -999,8 +1134,12 @@ public class FirebaseRemoteConfigClientImplTest {
         client.listVersions(null);
         fail("No error thrown for HTTP error");
       } catch (FirebaseRemoteConfigException error) {
-        checkExceptionFromHttpResponse(error, HTTP_STATUS_TO_ERROR_CODE.get(code), null,
-                "Unexpected HTTP response with status: " + code + "\n{}", HttpMethods.GET);
+        checkExceptionFromHttpResponse(
+            error,
+            HTTP_STATUS_TO_ERROR_CODE.get(code),
+            null,
+            "Unexpected HTTP response with status: " + code + "\n{}",
+            HttpMethods.GET);
       }
       checkGetRequestHeader(interceptor.getLastRequest(), ":listVersions");
     }
@@ -1015,8 +1154,8 @@ public class FirebaseRemoteConfigClientImplTest {
       fail("No error thrown for HTTP error");
     } catch (FirebaseRemoteConfigException error) {
       assertEquals(ErrorCode.UNKNOWN, error.getErrorCode());
-      assertEquals("Unknown error while making a remote service call: transport error",
-              error.getMessage());
+      assertEquals(
+          "Unknown error while making a remote service call: transport error", error.getMessage());
       assertTrue(error.getCause() instanceof IOException);
       assertNull(error.getHttpResponse());
       assertNull(error.getRemoteConfigErrorCode());
@@ -1049,8 +1188,12 @@ public class FirebaseRemoteConfigClientImplTest {
         client.listVersions(null);
         fail("No error thrown for HTTP error");
       } catch (FirebaseRemoteConfigException error) {
-        checkExceptionFromHttpResponse(error, HTTP_STATUS_TO_ERROR_CODE.get(code), null,
-                "Unexpected HTTP response with status: " + code + "\nnull", HttpMethods.GET);
+        checkExceptionFromHttpResponse(
+            error,
+            HTTP_STATUS_TO_ERROR_CODE.get(code),
+            null,
+            "Unexpected HTTP response with status: " + code + "\nnull",
+            HttpMethods.GET);
       }
       checkGetRequestHeader(interceptor.getLastRequest(), ":listVersions");
     }
@@ -1065,8 +1208,12 @@ public class FirebaseRemoteConfigClientImplTest {
         client.listVersions(null);
         fail("No error thrown for HTTP error");
       } catch (FirebaseRemoteConfigException error) {
-        checkExceptionFromHttpResponse(error, HTTP_STATUS_TO_ERROR_CODE.get(code), null,
-                "Unexpected HTTP response with status: " + code + "\nnot json", HttpMethods.GET);
+        checkExceptionFromHttpResponse(
+            error,
+            HTTP_STATUS_TO_ERROR_CODE.get(code),
+            null,
+            "Unexpected HTTP response with status: " + code + "\nnot json",
+            HttpMethods.GET);
       }
       checkGetRequestHeader(interceptor.getLastRequest(), ":listVersions");
     }
@@ -1075,15 +1222,17 @@ public class FirebaseRemoteConfigClientImplTest {
   @Test
   public void testListVersionsErrorWithDetails() {
     for (int code : HTTP_STATUS_CODES) {
-      response.setStatusCode(code).setContent(
+      response
+          .setStatusCode(code)
+          .setContent(
               "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\"}}");
 
       try {
         client.listVersions(null);
         fail("No error thrown for HTTP error");
       } catch (FirebaseRemoteConfigException error) {
-        checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, null, "test error",
-                HttpMethods.GET);
+        checkExceptionFromHttpResponse(
+            error, ErrorCode.INVALID_ARGUMENT, null, "test error", HttpMethods.GET);
       }
       checkGetRequestHeader(interceptor.getLastRequest(), ":listVersions");
     }
@@ -1092,17 +1241,22 @@ public class FirebaseRemoteConfigClientImplTest {
   @Test
   public void testListVersionsErrorWithRcError() {
     for (int code : HTTP_STATUS_CODES) {
-      response.setStatusCode(code).setContent(
+      response
+          .setStatusCode(code)
+          .setContent(
               "{\"error\": {\"status\": \"INVALID_ARGUMENT\", "
-                      + "\"message\": \"[INVALID_ARGUMENT]: test error\"}}");
+                  + "\"message\": \"[INVALID_ARGUMENT]: test error\"}}");
 
       try {
         client.listVersions(null);
         fail("No error thrown for HTTP error");
       } catch (FirebaseRemoteConfigException error) {
-        checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT,
-                RemoteConfigErrorCode.INVALID_ARGUMENT, "[INVALID_ARGUMENT]: test error",
-                HttpMethods.GET);
+        checkExceptionFromHttpResponse(
+            error,
+            ErrorCode.INVALID_ARGUMENT,
+            RemoteConfigErrorCode.INVALID_ARGUMENT,
+            "[INVALID_ARGUMENT]: test error",
+            HttpMethods.GET);
       }
       checkGetRequestHeader(interceptor.getLastRequest(), ":listVersions");
     }
@@ -1127,7 +1281,8 @@ public class FirebaseRemoteConfigClientImplTest {
 
   @Test
   public void testFromApp() throws IOException {
-    FirebaseOptions options = FirebaseOptions.builder()
+    FirebaseOptions options =
+        FirebaseOptions.builder()
             .setCredentials(new MockGoogleCredentials("test-token"))
             .setProjectId("test-project")
             .build();
@@ -1139,8 +1294,8 @@ public class FirebaseRemoteConfigClientImplTest {
       assertEquals(TEST_REMOTE_CONFIG_URL, client.getRemoteConfigUrl());
       assertSame(options.getJsonFactory(), client.getJsonFactory());
 
-      HttpRequest request = client.getRequestFactory().buildGetRequest(
-              new GenericUrl("https://example.com"));
+      HttpRequest request =
+          client.getRequestFactory().buildGetRequest(new GenericUrl("https://example.com"));
       assertEquals("Bearer test-token", request.getHeaders().getAuthorization());
     } finally {
       app.delete();
@@ -1148,33 +1303,32 @@ public class FirebaseRemoteConfigClientImplTest {
   }
 
   private FirebaseRemoteConfigClientImpl initRemoteConfigClient(
-          MockLowLevelHttpResponse mockResponse, HttpResponseInterceptor interceptor) {
-    MockHttpTransport transport = new MockHttpTransport.Builder()
-            .setLowLevelHttpResponse(mockResponse)
-            .build();
+      MockLowLevelHttpResponse mockResponse, HttpResponseInterceptor interceptor) {
+    MockHttpTransport transport =
+        new MockHttpTransport.Builder().setLowLevelHttpResponse(mockResponse).build();
 
     return FirebaseRemoteConfigClientImpl.builder()
-            .setProjectId("test-project")
-            .setJsonFactory(ApiClientUtils.getDefaultJsonFactory())
-            .setRequestFactory(transport.createRequestFactory())
-            .setResponseInterceptor(interceptor)
-            .build();
+        .setProjectId("test-project")
+        .setJsonFactory(ApiClientUtils.getDefaultJsonFactory())
+        .setRequestFactory(transport.createRequestFactory())
+        .setResponseInterceptor(interceptor)
+        .build();
   }
 
   private FirebaseRemoteConfigClientImpl initClientWithFaultyTransport() {
     HttpTransport transport = TestUtils.createFaultyHttpTransport();
     return FirebaseRemoteConfigClientImpl.builder()
-            .setProjectId("test-project")
-            .setJsonFactory(ApiClientUtils.getDefaultJsonFactory())
-            .setRequestFactory(transport.createRequestFactory())
-            .build();
+        .setProjectId("test-project")
+        .setJsonFactory(ApiClientUtils.getDefaultJsonFactory())
+        .setRequestFactory(transport.createRequestFactory())
+        .build();
   }
 
   private FirebaseRemoteConfigClientImpl.Builder fullyPopulatedBuilder() {
     return FirebaseRemoteConfigClientImpl.builder()
-            .setProjectId("test-project")
-            .setJsonFactory(ApiClientUtils.getDefaultJsonFactory())
-            .setRequestFactory(ApiClientUtils.getDefaultTransport().createRequestFactory());
+        .setProjectId("test-project")
+        .setJsonFactory(ApiClientUtils.getDefaultJsonFactory())
+        .setRequestFactory(ApiClientUtils.getDefaultTransport().createRequestFactory());
   }
 
   private void checkGetRequestHeader(HttpRequest request) {
@@ -1190,6 +1344,19 @@ public class FirebaseRemoteConfigClientImplTest {
     assertEquals("gzip", headers.getAcceptEncoding());
   }
 
+  private void checkGetRequestHeaderForServer(HttpRequest request) {
+    checkGetRequestHeaderForServer(request, "");
+  }
+
+  private void checkGetRequestHeaderForServer(HttpRequest request, String urlSuffix) {
+    assertEquals("GET", request.getRequestMethod());
+    assertEquals(TEST_SERVER_REMOTE_CONFIG_URL + urlSuffix, request.getUrl().toString());
+    HttpHeaders headers = request.getHeaders();
+    assertEquals("fire-admin-java/" + SdkUtils.getVersion(), headers.get("X-Firebase-Client"));
+    assertEquals(SdkUtils.getMetricsHeader(), request.getHeaders().get("X-Goog-Api-Client"));
+    assertEquals("gzip", headers.getAcceptEncoding());
+  }
+
   private void checkPutRequestHeader(HttpRequest request) {
     checkPutRequestHeader(request, "", TEST_ETAG);
   }
@@ -1213,8 +1380,8 @@ public class FirebaseRemoteConfigClientImplTest {
     assertEquals("gzip", headers.getAcceptEncoding());
   }
 
-  private void checkRequestContent(
-          HttpRequest request, Map<String, Object> expected) throws IOException {
+  private void checkRequestContent(HttpRequest request, Map<String, Object> expected)
+      throws IOException {
     ByteArrayOutputStream out = new ByteArrayOutputStream();
     request.getContent().writeTo(out);
     JsonParser parser = ApiClientUtils.getDefaultJsonFactory().createJsonParser(out.toString());
@@ -1224,11 +1391,11 @@ public class FirebaseRemoteConfigClientImplTest {
   }
 
   private void checkExceptionFromHttpResponse(
-          FirebaseRemoteConfigException error,
-          ErrorCode expectedCode,
-          RemoteConfigErrorCode expectedRemoteConfigCode,
-          String expectedMessage,
-          String httpMethod) {
+      FirebaseRemoteConfigException error,
+      ErrorCode expectedCode,
+      RemoteConfigErrorCode expectedRemoteConfigCode,
+      String expectedMessage,
+      String httpMethod) {
     assertEquals(expectedCode, error.getErrorCode());
     assertEquals(expectedMessage, error.getMessage());
     assertTrue(error.getCause() instanceof HttpResponseException);
@@ -1239,5 +1406,226 @@ public class FirebaseRemoteConfigClientImplTest {
     assertEquals(httpMethod, request.getMethod());
     assertTrue(request.getUrl().startsWith("https://firebaseremoteconfig.googleapis.com"));
   }
-}
 
+  // Get server template tests
+
+  @Test
+  public void testGetServerTemplate() throws Exception {
+    response.addHeader("etag", TEST_ETAG);
+    response.setContent(MOCK_SERVER_TEMPLATE_RESPONSE);
+
+    String receivedTemplate = client.getServerTemplate();
+    ServerTemplateData serverTemplateData = ServerTemplateData.fromJSON(receivedTemplate);
+
+    assertEquals(EXPECTED_PARAMETERS, serverTemplateData.getParameters());
+    assertEquals(TEST_ETAG, serverTemplateData.getETag());
+    assertEquals(
+        convertObjectToString(EXPECTED_SERVER_CONDITIONS),
+        convertObjectToString(serverTemplateData.getServerConditions()));
+    assertEquals(1605423446000L, serverTemplateData.getVersion().getUpdateTime());
+    checkGetRequestHeaderForServer(interceptor.getLastRequest());
+  }
+
+  @Test
+  public void testGetServerTemplateWithTimestampUpToNanosecondPrecision() throws Exception {
+    List<String> timestamps =
+        ImmutableList.of(
+            "2020-11-15T06:57:26.342Z",
+            "2020-11-15T06:57:26.342763Z",
+            "2020-11-15T06:57:26.342763941Z");
+    for (String timestamp : timestamps) {
+      response.addHeader("etag", TEST_ETAG);
+      String templateResponse =
+          "{\"version\": {"
+              + "    \"versionNumber\": \"17\","
+              + "    \"updateTime\": \""
+              + timestamp
+              + "\""
+              + "  }}";
+      response.setContent(templateResponse);
+
+      String receivedTemplate = client.getServerTemplate();
+      ServerTemplateData serverTemplateData = ServerTemplateData.fromJSON(receivedTemplate);
+
+      assertEquals(TEST_ETAG, serverTemplateData.getETag());
+      assertEquals("17", serverTemplateData.getVersion().getVersionNumber());
+      assertEquals(1605423446000L, serverTemplateData.getVersion().getUpdateTime());
+      checkGetRequestHeaderForServer(interceptor.getLastRequest());
+    }
+  }
+
+  @Test
+  public void testGetServerTemplateWithEmptyTemplateResponse() throws Exception {
+    response.addHeader("etag", TEST_ETAG);
+    response.setContent("{}");
+
+    String receivedTemplate = client.getServerTemplate();
+    ServerTemplateData serverTemplateData = ServerTemplateData.fromJSON(receivedTemplate);
+
+    assertEquals(TEST_ETAG, serverTemplateData.getETag());
+    assertEquals(0, serverTemplateData.getParameters().size());
+    assertEquals(0, serverTemplateData.getServerConditions().size());
+    assertEquals(0, serverTemplateData.getParameterGroups().size());
+    assertNull(serverTemplateData.getVersion());
+    checkGetRequestHeaderForServer(interceptor.getLastRequest());
+  }
+
+  @Test(expected = IllegalStateException.class)
+  public void testGetServerTemplateWithNoEtag() throws FirebaseRemoteConfigException {
+    // ETag does not exist
+    response.setContent(MOCK_SERVER_TEMPLATE_RESPONSE);
+
+    client.getServerTemplate();
+  }
+
+  @Test(expected = IllegalStateException.class)
+  public void testGetServerTemplateWithEmptyEtag() throws FirebaseRemoteConfigException {
+    // Empty ETag
+    response.addHeader("etag", "");
+    response.setContent(MOCK_SERVER_TEMPLATE_RESPONSE);
+
+    client.getServerTemplate();
+  }
+
+  @Test
+  public void testGetServerTemplateHttpError() {
+    for (int code : HTTP_STATUS_CODES) {
+      response.setStatusCode(code).setContent("{}");
+
+      try {
+        client.getServerTemplate();
+        fail("No error thrown for HTTP error");
+      } catch (FirebaseRemoteConfigException error) {
+        checkExceptionFromHttpResponse(
+            error,
+            HTTP_STATUS_TO_ERROR_CODE.get(code),
+            null,
+            "Unexpected HTTP response with status: " + code + "\n{}",
+            HttpMethods.GET);
+      }
+      checkGetRequestHeaderForServer(interceptor.getLastRequest());
+    }
+  }
+
+  @Test
+  public void testGetServerTemplateTransportError() {
+    client = initClientWithFaultyTransport();
+
+    try {
+      client.getServerTemplate();
+      fail("No error thrown for HTTP error");
+    } catch (FirebaseRemoteConfigException error) {
+      assertEquals(ErrorCode.UNKNOWN, error.getErrorCode());
+      assertEquals(
+          "Unknown error while making a remote service call: transport error", error.getMessage());
+      assertTrue(error.getCause() instanceof IOException);
+      assertNull(error.getHttpResponse());
+      assertNull(error.getRemoteConfigErrorCode());
+    }
+  }
+
+  @Test
+  public void testGetServerTemplateSuccessResponseWithUnexpectedPayload() {
+    response.setContent("not valid json");
+
+    try {
+      client.getServerTemplate();
+      fail("No error thrown for malformed response");
+    } catch (FirebaseRemoteConfigException error) {
+      assertEquals(ErrorCode.UNKNOWN, error.getErrorCode());
+      assertTrue(error.getMessage().startsWith("Error while parsing HTTP response: "));
+      assertNotNull(error.getCause());
+      assertNotNull(error.getHttpResponse());
+      assertNull(error.getRemoteConfigErrorCode());
+    }
+    checkGetRequestHeaderForServer(interceptor.getLastRequest());
+  }
+
+  @Test
+  public void testGetServerTemplateErrorWithZeroContentResponse() {
+    for (int code : HTTP_STATUS_CODES) {
+      response.setStatusCode(code).setZeroContent();
+
+      try {
+        client.getServerTemplate();
+        fail("No error thrown for HTTP error");
+      } catch (FirebaseRemoteConfigException error) {
+        checkExceptionFromHttpResponse(
+            error,
+            HTTP_STATUS_TO_ERROR_CODE.get(code),
+            null,
+            "Unexpected HTTP response with status: " + code + "\nnull",
+            HttpMethods.GET);
+      }
+      checkGetRequestHeaderForServer(interceptor.getLastRequest());
+    }
+  }
+
+  @Test
+  public void testGetServerTemplateErrorWithMalformedResponse() {
+    for (int code : HTTP_STATUS_CODES) {
+      response.setStatusCode(code).setContent("not json");
+
+      try {
+        client.getServerTemplate();
+        fail("No error thrown for HTTP error");
+      } catch (FirebaseRemoteConfigException error) {
+        checkExceptionFromHttpResponse(
+            error,
+            HTTP_STATUS_TO_ERROR_CODE.get(code),
+            null,
+            "Unexpected HTTP response with status: " + code + "\nnot json",
+            HttpMethods.GET);
+      }
+      checkGetRequestHeaderForServer(interceptor.getLastRequest());
+    }
+  }
+
+  @Test
+  public void testGetServerTemplateErrorWithDetails() {
+    for (int code : HTTP_STATUS_CODES) {
+      response
+          .setStatusCode(code)
+          .setContent(
+              "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\"}}");
+
+      try {
+        client.getServerTemplate();
+        fail("No error thrown for HTTP error");
+      } catch (FirebaseRemoteConfigException error) {
+        checkExceptionFromHttpResponse(
+            error, ErrorCode.INVALID_ARGUMENT, null, "test error", HttpMethods.GET);
+      }
+      checkGetRequestHeaderForServer(interceptor.getLastRequest());
+    }
+  }
+
+  @Test
+  public void testGetServerTemplateErrorWithRcError() {
+    for (int code : HTTP_STATUS_CODES) {
+      response
+          .setStatusCode(code)
+          .setContent(
+              "{\"error\": {\"status\": \"INVALID_ARGUMENT\", "
+                  + "\"message\": \"[INVALID_ARGUMENT]: test error\"}}");
+
+      try {
+        client.getServerTemplate();
+        fail("No error thrown for HTTP error");
+      } catch (FirebaseRemoteConfigException error) {
+        checkExceptionFromHttpResponse(
+            error,
+            ErrorCode.INVALID_ARGUMENT,
+            RemoteConfigErrorCode.INVALID_ARGUMENT,
+            "[INVALID_ARGUMENT]: test error",
+            HttpMethods.GET);
+      }
+      checkGetRequestHeaderForServer(interceptor.getLastRequest());
+    }
+  }
+
+  public static String convertObjectToString(Object object) {
+    Gson gson = new GsonBuilder().setPrettyPrinting().create(); // Optional: pretty printing
+    return gson.toJson(object);
+  }
+}
diff --git a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java
index 66821ab..f34035f 100644
--- a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java
+++ b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java
@@ -27,20 +27,18 @@ import com.google.firebase.FirebaseApp;
 import com.google.firebase.FirebaseOptions;
 import com.google.firebase.TestOnlyImplFirebaseTrampolines;
 import com.google.firebase.auth.MockGoogleCredentials;
-
 import com.google.firebase.remoteconfig.internal.TemplateResponse;
-
 import java.util.concurrent.ExecutionException;
-
 import org.junit.After;
 import org.junit.Test;
 
 /** Unit tests 
- * for {@link FirebaseRemoteConfig}. 
- * */
+* for {@link FirebaseRemoteConfig}. 
+* */
 public class FirebaseRemoteConfigTest {
 
-  private static final FirebaseOptions TEST_OPTIONS = FirebaseOptions.builder()
+  private static final FirebaseOptions TEST_OPTIONS =
+      FirebaseOptions.builder()
           .setCredentials(new MockGoogleCredentials("test-token"))
           .setProjectId("test-project")
           .build();
@@ -52,7 +50,7 @@ public class FirebaseRemoteConfigTest {
           + "  \"parameterGroups\": {}\n"
           + "}";
   private static final FirebaseRemoteConfigException TEST_EXCEPTION =
-          new FirebaseRemoteConfigException(ErrorCode.INTERNAL, "Test error message");
+      new FirebaseRemoteConfigException(ErrorCode.INTERNAL, "Test error message");
 
   @After
   public void tearDown() {
@@ -86,7 +84,8 @@ public class FirebaseRemoteConfigTest {
 
     assertTrue(client instanceof FirebaseRemoteConfigClientImpl);
     assertSame(client, remoteConfig.getRemoteConfigClient());
-    String expectedUrl = "https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/remoteConfig";
+    String expectedUrl =
+        "https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/remoteConfig";
     assertEquals(expectedUrl, ((FirebaseRemoteConfigClientImpl) client).getRemoteConfigUrl());
   }
 
@@ -122,16 +121,16 @@ public class FirebaseRemoteConfigTest {
 
   @Test
   public void testRemoteConfigClientWithoutProjectId() {
-    FirebaseOptions options = FirebaseOptions.builder()
-            .setCredentials(new MockGoogleCredentials("test-token"))
-            .build();
+    FirebaseOptions options =
+        FirebaseOptions.builder().setCredentials(new MockGoogleCredentials("test-token")).build();
     FirebaseApp.initializeApp(options);
 
     try {
       FirebaseRemoteConfig.getInstance();
       fail("No error thrown for missing project ID");
     } catch (IllegalArgumentException expected) {
-      String message = "Project ID is required to access Remote Config service. Use a service "
+      String message =
+          "Project ID is required to access Remote Config service. Use a service "
               + "account credential or set the project ID explicitly via FirebaseOptions. "
               + "Alternatively you can also set the project ID via the GOOGLE_CLOUD_PROJECT "
               + "environment variable.";
@@ -145,8 +144,8 @@ public class FirebaseRemoteConfigTest {
 
   @Test
   public void testGetTemplate() throws FirebaseRemoteConfigException {
-    MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(
-            new Template().setETag(TEST_ETAG));
+    MockRemoteConfigClient client =
+        MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG));
     FirebaseRemoteConfig remoteConfig = getRemoteConfig(client);
 
     Template template = remoteConfig.getTemplate();
@@ -168,8 +167,8 @@ public class FirebaseRemoteConfigTest {
 
   @Test
   public void testGetTemplateAsync() throws Exception {
-    MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(
-            new Template().setETag(TEST_ETAG));
+    MockRemoteConfigClient client =
+        MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG));
     FirebaseRemoteConfig remoteConfig = getRemoteConfig(client);
 
     Template template = remoteConfig.getTemplateAsync().get();
@@ -193,8 +192,8 @@ public class FirebaseRemoteConfigTest {
 
   @Test
   public void testGetTemplateAtVersionWithStringValue() throws FirebaseRemoteConfigException {
-    MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(
-            new Template().setETag(TEST_ETAG));
+    MockRemoteConfigClient client =
+        MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG));
     FirebaseRemoteConfig remoteConfig = getRemoteConfig(client);
 
     Template template = remoteConfig.getTemplateAtVersion("64");
@@ -216,8 +215,8 @@ public class FirebaseRemoteConfigTest {
 
   @Test
   public void testGetTemplateAtVersionAsyncWithStringValue() throws Exception {
-    MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(
-            new Template().setETag(TEST_ETAG));
+    MockRemoteConfigClient client =
+        MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG));
     FirebaseRemoteConfig remoteConfig = getRemoteConfig(client);
 
     Template template = remoteConfig.getTemplateAtVersionAsync("55").get();
@@ -239,8 +238,8 @@ public class FirebaseRemoteConfigTest {
 
   @Test
   public void testGetTemplateAtVersionWithLongValue() throws FirebaseRemoteConfigException {
-    MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(
-            new Template().setETag(TEST_ETAG));
+    MockRemoteConfigClient client =
+        MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG));
     FirebaseRemoteConfig remoteConfig = getRemoteConfig(client);
 
     Template template = remoteConfig.getTemplateAtVersion(64L);
@@ -262,8 +261,8 @@ public class FirebaseRemoteConfigTest {
 
   @Test
   public void testGetTemplateAtVersionAsyncWithLongValue() throws Exception {
-    MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(
-            new Template().setETag(TEST_ETAG));
+    MockRemoteConfigClient client =
+        MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG));
     FirebaseRemoteConfig remoteConfig = getRemoteConfig(client);
 
     Template template = remoteConfig.getTemplateAtVersionAsync(55L).get();
@@ -431,8 +430,8 @@ public class FirebaseRemoteConfigTest {
 
   @Test
   public void testRollbackWithStringValue() throws FirebaseRemoteConfigException {
-    MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(
-            new Template().setETag(TEST_ETAG));
+    MockRemoteConfigClient client =
+        MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG));
     FirebaseRemoteConfig remoteConfig = getRemoteConfig(client);
 
     Template template = remoteConfig.rollback("64");
@@ -454,8 +453,8 @@ public class FirebaseRemoteConfigTest {
 
   @Test
   public void testRollbackAsyncWithStringValue() throws Exception {
-    MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(
-            new Template().setETag(TEST_ETAG));
+    MockRemoteConfigClient client =
+        MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG));
     FirebaseRemoteConfig remoteConfig = getRemoteConfig(client);
 
     Template template = remoteConfig.rollbackAsync("55").get();
@@ -477,8 +476,8 @@ public class FirebaseRemoteConfigTest {
 
   @Test
   public void testRollbackWithLongValue() throws FirebaseRemoteConfigException {
-    MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(
-            new Template().setETag(TEST_ETAG));
+    MockRemoteConfigClient client =
+        MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG));
     FirebaseRemoteConfig remoteConfig = getRemoteConfig(client);
 
     Template template = remoteConfig.rollback(64L);
@@ -500,8 +499,8 @@ public class FirebaseRemoteConfigTest {
 
   @Test
   public void testRollbackAsyncWithLongValue() throws Exception {
-    MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(
-            new Template().setETag(TEST_ETAG));
+    MockRemoteConfigClient client =
+        MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG));
     FirebaseRemoteConfig remoteConfig = getRemoteConfig(client);
 
     Template template = remoteConfig.rollbackAsync(55L).get();
@@ -525,7 +524,8 @@ public class FirebaseRemoteConfigTest {
 
   @Test
   public void testListVersionsWithNoOptions() throws FirebaseRemoteConfigException {
-    MockRemoteConfigClient client = MockRemoteConfigClient.fromListVersionsResponse(
+    MockRemoteConfigClient client =
+        MockRemoteConfigClient.fromListVersionsResponse(
             new TemplateResponse.ListVersionsResponse().setNextPageToken("token"));
     FirebaseRemoteConfig remoteConfig = getRemoteConfig(client);
 
@@ -548,7 +548,8 @@ public class FirebaseRemoteConfigTest {
 
   @Test
   public void testListVersionsAsyncWithNoOptions() throws Exception {
-    MockRemoteConfigClient client = MockRemoteConfigClient.fromListVersionsResponse(
+    MockRemoteConfigClient client =
+        MockRemoteConfigClient.fromListVersionsResponse(
             new TemplateResponse.ListVersionsResponse().setNextPageToken("token"));
     FirebaseRemoteConfig remoteConfig = getRemoteConfig(client);
 
@@ -571,12 +572,13 @@ public class FirebaseRemoteConfigTest {
 
   @Test
   public void testListVersionsWithOptions() throws FirebaseRemoteConfigException {
-    MockRemoteConfigClient client = MockRemoteConfigClient.fromListVersionsResponse(
+    MockRemoteConfigClient client =
+        MockRemoteConfigClient.fromListVersionsResponse(
             new TemplateResponse.ListVersionsResponse().setNextPageToken("token"));
     FirebaseRemoteConfig remoteConfig = getRemoteConfig(client);
 
-    ListVersionsPage listVersionsPage = remoteConfig.listVersions(
-            ListVersionsOptions.builder().build());
+    ListVersionsPage listVersionsPage =
+        remoteConfig.listVersions(ListVersionsOptions.builder().build());
 
     assertEquals("token", listVersionsPage.getNextPageToken());
   }
@@ -595,12 +597,13 @@ public class FirebaseRemoteConfigTest {
 
   @Test
   public void testListVersionsAsyncWithOptions() throws Exception {
-    MockRemoteConfigClient client = MockRemoteConfigClient.fromListVersionsResponse(
+    MockRemoteConfigClient client =
+        MockRemoteConfigClient.fromListVersionsResponse(
             new TemplateResponse.ListVersionsResponse().setNextPageToken("token"));
     FirebaseRemoteConfig remoteConfig = getRemoteConfig(client);
 
-    ListVersionsPage listVersionsPage = remoteConfig.listVersionsAsync(
-            ListVersionsOptions.builder().build()).get();
+    ListVersionsPage listVersionsPage =
+        remoteConfig.listVersionsAsync(ListVersionsOptions.builder().build()).get();
 
     assertEquals("token", listVersionsPage.getNextPageToken());
   }
diff --git a/src/test/java/com/google/firebase/remoteconfig/MockRemoteConfigClient.java b/src/test/java/com/google/firebase/remoteconfig/MockRemoteConfigClient.java
index 1e7c0d4..3ac7f6b 100644
--- a/src/test/java/com/google/firebase/remoteconfig/MockRemoteConfigClient.java
+++ b/src/test/java/com/google/firebase/remoteconfig/MockRemoteConfigClient.java
@@ -26,9 +26,9 @@ public class MockRemoteConfigClient implements FirebaseRemoteConfigClient{
   private final ListVersionsResponse listVersionsResponse;
 
   private MockRemoteConfigClient(Template resultTemplate,
-                                 String resultServerTemplate,
-                                 ListVersionsResponse listVersionsResponse,
-                                 FirebaseRemoteConfigException exception) {
+                                String resultServerTemplate,
+                                ListVersionsResponse listVersionsResponse,
+                                FirebaseRemoteConfigException exception) {
     this.resultTemplate = resultTemplate;
     this.resultServerTemplate = resultServerTemplate;
     this.listVersionsResponse = listVersionsResponse;
@@ -102,4 +102,3 @@ public class MockRemoteConfigClient implements FirebaseRemoteConfigClient{
     return listVersionsResponse;
   }
 }
-
diff --git a/src/test/java/com/google/firebase/remoteconfig/ParameterValueTest.java b/src/test/java/com/google/firebase/remoteconfig/ParameterValueTest.java
deleted file mode 100644
index 842fd80..0000000
--- a/src/test/java/com/google/firebase/remoteconfig/ParameterValueTest.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.google.firebase.remoteconfig;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotEquals;
-
-import org.junit.Test;
-
-public class ParameterValueTest {
-
-  @Test
-  public void testCreateExplicitValue() {
-    final ParameterValue.Explicit parameterValue = ParameterValue.of("title text");
-
-    assertEquals("title text", parameterValue.getValue());
-  }
-
-  @Test
-  public void testCreateInAppDefault() {
-    final ParameterValue.InAppDefault parameterValue = ParameterValue.inAppDefault();
-
-    assertEquals(ParameterValue.InAppDefault.class, parameterValue.getClass());
-  }
-
-  @Test
-  public void testEquality() {
-    ParameterValue.Explicit parameterValueOne = ParameterValue.of("value");
-    ParameterValue.Explicit parameterValueTwo = ParameterValue.of("value");
-    ParameterValue.Explicit parameterValueThree = ParameterValue.of("title");
-
-    assertEquals(parameterValueOne, parameterValueTwo);
-    assertNotEquals(parameterValueOne, parameterValueThree);
-
-    ParameterValue.InAppDefault parameterValueFour = ParameterValue.inAppDefault();
-    ParameterValue.InAppDefault parameterValueFive = ParameterValue.inAppDefault();
-
-    assertEquals(parameterValueFour, parameterValueFive);
-  }
-}
diff --git a/src/test/java/com/google/firebase/remoteconfig/ServerConditionTest.java b/src/test/java/com/google/firebase/remoteconfig/ServerConditionTest.java
new file mode 100644
index 0000000..2a51579
--- /dev/null
+++ b/src/test/java/com/google/firebase/remoteconfig/ServerConditionTest.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.firebase.remoteconfig;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+
+import com.google.common.collect.ImmutableList;
+import com.google.firebase.remoteconfig.internal.ServerTemplateResponse.CustomSignalConditionResponse;
+import com.google.firebase.remoteconfig.internal.ServerTemplateResponse.OneOfConditionResponse;
+import com.google.firebase.remoteconfig.internal.ServerTemplateResponse.ServerConditionResponse;
+import java.util.ArrayList;
+import org.junit.Test;
+
+/** Tests 
+* for {@link ServerCondition}. 
+* */
+public class ServerConditionTest {
+
+  @Test
+  public void testConstructor() {
+    OneOfCondition conditions =
+        new OneOfCondition()
+            .setOrCondition(
+                new OrCondition(
+                    ImmutableList.of(
+                        new OneOfCondition()
+                            .setAndCondition(
+                                new AndCondition(
+                                    ImmutableList.of(
+                                        new OneOfCondition()
+                                            .setCustomSignal(
+                                                new CustomSignalCondition(
+                                                    "users",
+                                                    CustomSignalOperator.NUMERIC_LESS_THAN,
+                                                    new ArrayList<>(
+                                                        ImmutableList.of("100"))))))))));
+    ServerCondition serverCondition = new ServerCondition("ios_en_1", conditions);
+
+    assertEquals("ios_en_1", serverCondition.getName());
+    assertEquals(conditions, serverCondition.getCondition());
+  }
+
+  @Test
+  public void testConstructorWithResponse() {
+    CustomSignalConditionResponse customResponse =
+        new CustomSignalConditionResponse()
+            .setKey("test_key")
+            .setOperator("NUMERIC_EQUAL")
+            .setTargetValues(ImmutableList.of("1"));
+    OneOfConditionResponse conditionResponse =
+        new OneOfConditionResponse().setCustomSignalCondition(customResponse);
+    ServerConditionResponse response =
+        new ServerConditionResponse().setName("ios_en_2").setServerCondition(conditionResponse);
+    ServerCondition serverCondition = new ServerCondition(response);
+
+    assertEquals("ios_en_2", serverCondition.getName());
+    assertEquals("test_key", serverCondition.getCondition().getCustomSignal().getCustomSignalKey());
+  }
+
+  @Test
+  public void testIllegalConstructor() {
+    IllegalArgumentException error =
+        assertThrows(IllegalArgumentException.class, () -> new ServerCondition(null, null));
+
+    assertEquals("condition name must not be null or empty", error.getMessage());
+  }
+
+  @Test
+  public void testConstructorWithNullServerConditionResponse() {
+    assertThrows(NullPointerException.class, () -> new ServerCondition(null));
+  }
+
+  @Test
+  public void testSetNullName() {
+    OneOfCondition conditions =
+        new OneOfCondition()
+            .setOrCondition(
+                new OrCondition(
+                    ImmutableList.of(
+                        new OneOfCondition()
+                            .setAndCondition(
+                                new AndCondition(
+                                    ImmutableList.of(
+                                        new OneOfCondition()
+                                            .setCustomSignal(
+                                                new CustomSignalCondition(
+                                                    "users",
+                                                    CustomSignalOperator.NUMERIC_LESS_THAN,
+                                                    new ArrayList<>(
+                                                        ImmutableList.of("100"))))))))));
+    ServerCondition condition = new ServerCondition("ios", conditions);
+    
+    IllegalArgumentException error = assertThrows(IllegalArgumentException.class,
+        () -> condition.setName(null));
+
+    assertEquals("condition name must not be null or empty", error.getMessage());
+  }
+
+  @Test
+  public void testSetEmptyName() {
+    OneOfCondition conditions =
+        new OneOfCondition()
+            .setOrCondition(
+                new OrCondition(
+                    ImmutableList.of(
+                        new OneOfCondition()
+                            .setAndCondition(
+                                new AndCondition(
+                                    ImmutableList.of(
+                                        new OneOfCondition()
+                                            .setCustomSignal(
+                                                new CustomSignalCondition(
+                                                    "users",
+                                                    CustomSignalOperator.NUMERIC_LESS_THAN,
+                                                    new ArrayList<>(
+                                                        ImmutableList.of("100"))))))))));
+    ServerCondition condition = new ServerCondition("ios", conditions);
+    
+    IllegalArgumentException error = assertThrows(IllegalArgumentException.class,
+        () -> condition.setName(""));
+
+    assertEquals("condition name must not be null or empty", error.getMessage());
+  }
+
+  @Test
+  public void testSetNullServerCondition() {
+    OneOfCondition conditions =
+        new OneOfCondition()
+            .setOrCondition(
+                new OrCondition(
+                    ImmutableList.of(
+                        new OneOfCondition()
+                            .setAndCondition(
+                                new AndCondition(
+                                    ImmutableList.of(
+                                        new OneOfCondition()
+                                            .setCustomSignal(
+                                                new CustomSignalCondition(
+                                                    "users",
+                                                    CustomSignalOperator.NUMERIC_LESS_THAN,
+                                                    new ArrayList<>(
+                                                        ImmutableList.of("100"))))))))));
+    ServerCondition condition = new ServerCondition("ios", conditions);
+    
+    assertThrows(NullPointerException.class, () -> condition.setServerCondition(null));
+  }
+
+  @Test
+  public void testEquality() {
+    OneOfCondition conditionOne =
+        new OneOfCondition()
+            .setOrCondition(
+                new OrCondition(
+                    ImmutableList.of(
+                        new OneOfCondition()
+                            .setAndCondition(
+                                new AndCondition(
+                                    ImmutableList.of(
+                                        new OneOfCondition()
+                                            .setCustomSignal(
+                                                new CustomSignalCondition(
+                                                    "users",
+                                                    CustomSignalOperator.NUMERIC_LESS_THAN,
+                                                    new ArrayList<>(
+                                                        ImmutableList.of("100"))))))))));
+    OneOfCondition conditionTwo =
+        new OneOfCondition()
+            .setOrCondition(
+                new OrCondition(
+                    ImmutableList.of(
+                        new OneOfCondition()
+                            .setAndCondition(
+                                new AndCondition(
+                                    ImmutableList.of(
+                                        new OneOfCondition()
+                                            .setCustomSignal(
+                                                new CustomSignalCondition(
+                                                    "users",
+                                                    CustomSignalOperator.NUMERIC_LESS_THAN,
+                                                    new ArrayList<>(ImmutableList.of("100")))),
+                                                    new OneOfCondition()
+                                                    .setCustomSignal(
+                                                        new CustomSignalCondition(
+                                                            "users",
+                                                            CustomSignalOperator
+                                                                .NUMERIC_GREATER_THAN,
+                                                            new ArrayList<>(ImmutableList.of("20"))
+                                                        ))))))));
+
+    final ServerCondition serverConditionOne = new ServerCondition("ios", conditionOne);
+    final ServerCondition serverConditionTwo = new ServerCondition("ios", conditionOne);
+    final ServerCondition serverConditionThree = new ServerCondition("android", conditionTwo);
+    final ServerCondition serverConditionFour = new ServerCondition("android", conditionTwo);
+
+    assertEquals(serverConditionOne, serverConditionTwo);
+    assertEquals(serverConditionThree, serverConditionFour);
+  }
+}
diff --git a/src/test/java/com/google/firebase/remoteconfig/ServerTemplateImplTest.java b/src/test/java/com/google/firebase/remoteconfig/ServerTemplateImplTest.java
new file mode 100644
index 0000000..05a6b28
--- /dev/null
+++ b/src/test/java/com/google/firebase/remoteconfig/ServerTemplateImplTest.java
@@ -0,0 +1,287 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.firebase.remoteconfig;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+
+import com.google.api.core.ApiFuture;
+import com.google.firebase.FirebaseApp;
+import com.google.firebase.FirebaseOptions;
+import com.google.firebase.auth.MockGoogleCredentials;
+import com.google.firebase.testing.TestUtils;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+* Tests for {@link ServerTemplateImpl}.
+*/
+public class ServerTemplateImplTest {
+
+  private static final FirebaseOptions TEST_OPTIONS =
+      FirebaseOptions.builder()
+          .setCredentials(new MockGoogleCredentials("test-token"))
+          .setProjectId("test-project")
+          .build();
+  private static final String TEST_SERVER_TEMPLATE =
+        "{\n"
+            + "  \"etag\": \"etag-123456789012-1\",\n"
+            + "  \"parameters\": {},\n"
+            + "  \"serverConditions\": [],\n"
+            + "  \"parameterGroups\": {}\n"
+            + "}";
+
+  private static String cacheTemplate;
+
+  @BeforeClass
+  public static void setUpClass() {
+    cacheTemplate = TestUtils.loadResource("getServerTemplateData.json");
+  }
+
+  @Test
+  public void testServerTemplateWithoutCacheValueThrowsException()
+      throws FirebaseRemoteConfigException {
+    KeysAndValues defaultConfig = new KeysAndValues.Builder().build();
+
+    IllegalArgumentException error = assertThrows(IllegalArgumentException.class, 
+        () ->  new ServerTemplateImpl.Builder(null).defaultConfig(defaultConfig).build());
+
+    assertEquals("JSON String must not be null or empty.", error.getMessage());
+  }
+
+  @Test
+  public void testEvaluateCustomSignalReturnsDefaultValue() throws FirebaseRemoteConfigException {
+    KeysAndValues defaultConfig = new KeysAndValues.Builder().build();
+    KeysAndValues context = new KeysAndValues.Builder().put("users", "100").build();
+    ServerTemplate template =
+        new ServerTemplateImpl.Builder(null)
+            .defaultConfig(defaultConfig)
+            .cachedTemplate(cacheTemplate)
+            .build();
+
+    ServerConfig evaluatedConfig = template.evaluate(context);
+
+    assertEquals("Default value", evaluatedConfig.getString("Custom"));
+  }
+
+  @Test
+  public void testEvaluateCustomSignalReturnsConditionalValue()
+      throws FirebaseRemoteConfigException {
+    KeysAndValues defaultConfig = new KeysAndValues.Builder().build();
+    KeysAndValues context = new KeysAndValues.Builder().put("users", "99").build();
+    ServerTemplate template =
+        new ServerTemplateImpl.Builder(null)
+            .defaultConfig(defaultConfig)
+            .cachedTemplate(cacheTemplate)
+            .build();
+
+    ServerConfig evaluatedConfig = template.evaluate(context);
+
+    assertEquals("Conditional value", evaluatedConfig.getString("Custom"));
+  }
+
+  @Test
+  public void testEvaluateCustomSignalWithoutContextReturnsDefaultValue()
+      throws FirebaseRemoteConfigException {
+    KeysAndValues defaultConfig = new KeysAndValues.Builder().build();
+    KeysAndValues context = new KeysAndValues.Builder().build();
+    ServerTemplate template =
+        new ServerTemplateImpl.Builder(null)
+            .defaultConfig(defaultConfig)
+            .cachedTemplate(cacheTemplate)
+            .build();
+
+    ServerConfig evaluatedConfig = template.evaluate(context);
+
+    assertEquals("Default value", evaluatedConfig.getString("Custom"));
+  }
+
+  @Test
+  public void testEvaluateCustomSignalWithInvalidContextReturnsDefaultValue()
+      throws FirebaseRemoteConfigException {
+    KeysAndValues defaultConfig = new KeysAndValues.Builder().build();
+    KeysAndValues context = new KeysAndValues.Builder().put("users", "abc").build();
+    ServerTemplate template =
+        new ServerTemplateImpl.Builder(null)
+            .defaultConfig(defaultConfig)
+            .cachedTemplate(cacheTemplate)
+            .build();
+
+    ServerConfig evaluatedConfig = template.evaluate(context);
+
+    assertEquals("Default value", evaluatedConfig.getString("Custom"));
+  }
+
+  @Test
+  public void testEvaluateWithoutDefaultValueReturnsEmptyString()
+      throws FirebaseRemoteConfigException {
+    KeysAndValues defaultConfig = new KeysAndValues.Builder().build();
+    KeysAndValues context = new KeysAndValues.Builder().build();
+    ServerTemplate template =
+        new ServerTemplateImpl.Builder(null)
+            .defaultConfig(defaultConfig)
+            .cachedTemplate(cacheTemplate)
+            .build();
+
+    ServerConfig evaluatedConfig = template.evaluate(context);
+
+    assertEquals("", evaluatedConfig.getString("Unset default value"));
+  }
+
+  @Test
+  public void testEvaluateWithInvalidCacheValueThrowsException()
+      throws FirebaseRemoteConfigException {
+    KeysAndValues defaultConfig = new KeysAndValues.Builder().build();
+    KeysAndValues context = new KeysAndValues.Builder().build();
+    String invalidJsonString = "abc";
+    ServerTemplate template =
+        new ServerTemplateImpl.Builder(null)
+            .defaultConfig(defaultConfig)
+            .cachedTemplate(invalidJsonString)
+            .build();
+
+    FirebaseRemoteConfigException error = assertThrows(FirebaseRemoteConfigException.class,
+        () -> template.evaluate(context));
+
+    assertEquals("No Remote Config Server template in cache. Call load() before "
+        + "calling evaluate().", error.getMessage());
+  }
+
+  @Test
+  public void testEvaluateWithInAppDefaultReturnsEmptyString() throws Exception {
+    KeysAndValues defaultConfig = new KeysAndValues.Builder().build();
+    KeysAndValues context = new KeysAndValues.Builder().build();
+    ServerTemplate template =
+        new ServerTemplateImpl.Builder(null)
+            .defaultConfig(defaultConfig)
+            .cachedTemplate(cacheTemplate)
+            .build();
+
+    ServerConfig evaluatedConfig = template.evaluate(context);
+
+    assertEquals("", evaluatedConfig.getString("In-app default"));
+  }
+
+  @Test
+  public void testEvaluateWithChainedAndConditionReturnsDefaultValue() throws Exception {
+    KeysAndValues defaultConfig = new KeysAndValues.Builder().build();
+    KeysAndValues context =
+        new KeysAndValues.Builder().put("users", "100").put("premium users", "20").build();
+    ServerTemplate template =
+        new ServerTemplateImpl.Builder(null)
+            .defaultConfig(defaultConfig)
+            .cachedTemplate(cacheTemplate)
+            .build();
+
+    ServerConfig evaluatedConfig = template.evaluate(context);
+
+    assertEquals("Default value", evaluatedConfig.getString("Chained conditions"));
+  }
+
+  @Test
+  public void testEvaluateWithChainedAndConditionReturnsConditionalValue() throws Exception {
+    KeysAndValues defaultConfig = new KeysAndValues.Builder().build();
+    KeysAndValues context =
+        new KeysAndValues.Builder().put("users", "99").put("premium users", "30").build();
+    ServerTemplate template =
+        new ServerTemplateImpl.Builder(null)
+            .defaultConfig(defaultConfig)
+            .cachedTemplate(cacheTemplate)
+            .build();
+
+    ServerConfig evaluatedConfig = template.evaluate(context);
+
+    assertEquals("Conditional value", evaluatedConfig.getString("Chained conditions"));
+  }
+
+  @Test
+  public void testGetEvaluateConfigInvalidKeyReturnsStaticValueSource() throws Exception {
+    KeysAndValues defaultConfig = new KeysAndValues.Builder().build();
+    KeysAndValues context = new KeysAndValues.Builder().build();
+    ServerTemplate template =
+        new ServerTemplateImpl.Builder(null)
+            .defaultConfig(defaultConfig)
+            .cachedTemplate(cacheTemplate)
+            .build();
+
+    ServerConfig evaluatedConfig = template.evaluate(context);
+
+    assertEquals(ValueSource.STATIC, evaluatedConfig.getValueSource("invalid"));
+  }
+
+  @Test
+  public void testGetEvaluateConfigInAppDefaultConfigReturnsDefaultValueSource() throws Exception {
+    KeysAndValues defaultConfig = new KeysAndValues.Builder().put("In-app default", "abc").build();
+    KeysAndValues context = new KeysAndValues.Builder().build();
+    ServerTemplate template =
+        new ServerTemplateImpl.Builder(null)
+            .defaultConfig(defaultConfig)
+            .cachedTemplate(cacheTemplate)
+            .build();
+
+    ServerConfig evaluatedConfig = template.evaluate(context);
+
+    assertEquals(ValueSource.DEFAULT, evaluatedConfig.getValueSource("In-app default"));
+  }
+
+  @Test
+  public void testGetEvaluateConfigUnsetDefaultConfigReturnsDefaultValueSource() throws Exception {
+    KeysAndValues defaultConfig =
+        new KeysAndValues.Builder().put("Unset default config", "abc").build();
+    KeysAndValues context = new KeysAndValues.Builder().build();
+    ServerTemplate template =
+        new ServerTemplateImpl.Builder(null)
+            .defaultConfig(defaultConfig)
+            .cachedTemplate(cacheTemplate)
+            .build();
+
+    ServerConfig evaluatedConfig = template.evaluate(context);
+
+    assertEquals(ValueSource.DEFAULT, evaluatedConfig.getValueSource("Unset default config"));
+  }
+
+  private static final String TEST_ETAG = "etag-123456789012-1";
+
+  private FirebaseRemoteConfig getRemoteConfig(FirebaseRemoteConfigClient client) {
+    FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS, "test-app");
+    return new FirebaseRemoteConfig(app, client);
+  }
+
+  @Test
+  public void testLoad() throws Exception {
+    KeysAndValues defaultConfig =
+        new KeysAndValues.Builder().put("Unset default config", "abc").build();
+
+    // Mock the HTTP client to return a predefined response
+    MockRemoteConfigClient client =
+        MockRemoteConfigClient.fromServerTemplate(
+            new ServerTemplateData().setETag(TEST_ETAG).toJSON());
+    FirebaseRemoteConfig remoteConfig = getRemoteConfig(client);
+    ServerTemplate template1 =
+        remoteConfig
+            .serverTemplateBuilder()
+            .defaultConfig(defaultConfig)
+            .cachedTemplate(cacheTemplate)
+            .build();
+
+    // Call the load method
+    ApiFuture<Void> loadFuture = template1.load();
+    loadFuture.get();
+    String cachedTemplate = template1.toJson();
+    assertEquals(TEST_SERVER_TEMPLATE, cachedTemplate);
+  }
+}
diff --git a/src/test/java/com/google/firebase/remoteconfig/ValueTest.java b/src/test/java/com/google/firebase/remoteconfig/ValueTest.java
new file mode 100644
index 0000000..c1822b0
--- /dev/null
+++ b/src/test/java/com/google/firebase/remoteconfig/ValueTest.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.firebase.remoteconfig;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+public class ValueTest {
+  @Test
+  public void testGetSourceReturnsValueSource() {
+    Value value = new Value(ValueSource.STATIC);
+    assertEquals(value.getSource(), ValueSource.STATIC);
+  }
+
+  @Test
+  public void testAsStringReturnsValueAsString() {
+    Value value = new Value(ValueSource.STATIC, "sample-string");
+    assertEquals(value.asString(), "sample-string");
+  }
+
+  @Test
+  public void testAsStringReturnsDefaultEmptyString() {
+    Value value = new Value(ValueSource.STATIC);
+    assertEquals(value.asString(), "");
+  }
+
+  @Test
+  public void testAsLongReturnsDefaultValueForStaticSource() {
+    Value value = new Value(ValueSource.STATIC);
+    assertEquals(value.asLong(), 0L);
+  }
+
+  @Test
+  public void testAsLongReturnsDefaultValueForInvalidSourceValue() {
+    Value value = new Value(ValueSource.REMOTE, "sample-string");
+    assertEquals(value.asLong(), 0L);
+  }
+
+  @Test
+  public void testAsLongReturnsSourceValueAsLong() {
+    Value value = new Value(ValueSource.REMOTE, "123");
+    assertEquals(value.asLong(), 123L);
+  }
+
+  @Test
+  public void testAsDoubleReturnsDefaultValueForStaticSource() {
+    Value value = new Value(ValueSource.STATIC);
+    assertEquals(value.asDouble(), 0, 0);
+  }
+
+  @Test
+  public void testAsDoubleReturnsDefaultValueForInvalidSourceValue() {
+    Value value = new Value(ValueSource.REMOTE, "sample-string");
+    assertEquals(value.asDouble(), 0, 0);
+  }
+
+  @Test
+  public void testAsDoubleReturnsSourceValueAsDouble() {
+    Value value = new Value(ValueSource.REMOTE, "123.34");
+    assertEquals(value.asDouble(), 123.34, 0);
+  }
+  
+  @Test
+  public void testAsBooleanReturnsDefaultValueForStaticSource() {
+    Value value = new Value(ValueSource.STATIC);
+    assertFalse(value.asBoolean());
+  }
+
+  @Test
+  public void testAsBooleanReturnsDefaultValueForInvalidSourceValue() {
+    Value value = new Value(ValueSource.REMOTE, "sample-string");
+    assertFalse(value.asBoolean());
+  }
+
+  @Test
+  public void testAsBooleanReturnsSourceValueAsBoolean() {
+    Value value = new Value(ValueSource.REMOTE, "1");
+    assertTrue(value.asBoolean());
+  }
+
+  @Test
+  public void testAsBooleanReturnsSourceValueYesAsBoolean() {
+    Value value = new Value(ValueSource.REMOTE, "YeS");
+    assertTrue(value.asBoolean());
+  }
+}
+ 
\ No newline at end of file
diff --git a/src/test/java/com/google/firebase/remoteconfig/VersionTest.java b/src/test/java/com/google/firebase/remoteconfig/VersionTest.java
index 5156296..1e52419 100644
--- a/src/test/java/com/google/firebase/remoteconfig/VersionTest.java
+++ b/src/test/java/com/google/firebase/remoteconfig/VersionTest.java
@@ -20,6 +20,7 @@ import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
 
 import com.google.firebase.remoteconfig.internal.TemplateResponse;
 import com.google.firebase.remoteconfig.internal.TemplateResponse.VersionResponse;
@@ -27,15 +28,16 @@ import org.junit.Test;
 
 public class VersionTest {
 
-  @Test(expected = NullPointerException.class)
+  @Test
   public void testConstructorWithNullVersionResponse() {
-    new Version(null);
+    assertThrows(NullPointerException.class, () -> new Version(null));
   }
 
-  @Test(expected = IllegalStateException.class)
+  @Test
   public void testConstructorWithInvalidUpdateTime() {
-    new Version(new VersionResponse()
-            .setUpdateTime("sunday,26th"));
+    assertThrows(IllegalStateException.class, () -> 
+        new Version(new VersionResponse().setUpdateTime("sunday,26th")));
+
   }
 
   @Test
diff --git a/src/test/resources/getServerRemoteConfig.json b/src/test/resources/getServerRemoteConfig.json
new file mode 100644
index 0000000..afc4c55
--- /dev/null
+++ b/src/test/resources/getServerRemoteConfig.json
@@ -0,0 +1,107 @@
+{
+  "conditions": [
+    {
+      "name": "custom_signal",
+      "condition": {
+        "orCondition": {
+          "conditions": [
+            {
+              "andCondition": {
+                "conditions": [
+                  {
+                    "customSignal": {
+                      "customSignalOperator": "NUMERIC_LESS_THAN",
+                      "customSignalKey": "users",
+                      "targetCustomSignalValues": [
+                        "100"
+                      ]
+                    }
+                  }
+                ]
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "name": "chained_conditions",
+      "condition": {
+        "orCondition": {
+          "conditions": [
+            {
+              "andCondition": {
+                "conditions": [
+                  {
+                    "customSignal": {
+                      "customSignalOperator": "NUMERIC_LESS_THAN",
+                      "customSignalKey": "users",
+                      "targetCustomSignalValues": [
+                        "100"
+                      ]
+                    }
+                  },
+                  {
+                    "customSignal": {
+                      "customSignalOperator": "NUMERIC_GREATER_THAN",
+                      "customSignalKey": "premium users",
+                      "targetCustomSignalValues": [
+                        "20"
+                      ]
+                    }
+                  }
+                ]
+              }
+            }
+          ]
+        }
+      }
+    }
+  ],
+  "parameters": {
+    "welcome_message_text": {
+      "defaultValue": {
+        "value": "welcome to app"
+      },
+      "conditionalValues": {
+        "ios_en": {
+          "value": "welcome to app en"
+        }
+      },
+      "description": "text for welcome message!",
+      "valueType": "STRING"
+    },
+    "header_text": {
+      "defaultValue": {
+        "useInAppDefault": true
+      },
+      "valueType": "STRING"
+    }
+  },
+  "parameterGroups": {
+    "new menu": {
+      "description": "New Menu",
+      "parameters": {
+        "pumpkin_spice_season": {
+          "defaultValue": {
+            "value": "true"
+          },
+          "description": "Whether it's currently pumpkin spice season.",
+          "valueType": "BOOLEAN"
+        }
+      }
+    }
+  },
+  "version": {
+    "versionNumber": "17",
+    "updateOrigin": "ADMIN_SDK_NODE",
+    "updateType": "INCREMENTAL_UPDATE",
+    "updateUser": {
+      "email": "firebase-user@account.com",
+      "name": "dev-admin",
+      "imageUrl": "http://image.jpg"
+    },
+    "updateTime": "2020-11-15T06:57:26.342763941Z",
+    "description": "promo config"
+  }
+}
diff --git a/src/test/resources/getServerTemplateData.json b/src/test/resources/getServerTemplateData.json
new file mode 100644
index 0000000..64731ef
--- /dev/null
+++ b/src/test/resources/getServerTemplateData.json
@@ -0,0 +1,97 @@
+{
+  "conditions": [
+    {
+      "name": "custom_signal",
+      "condition": {
+        "orCondition": {
+          "conditions": [
+            {
+              "andCondition": {
+                "conditions": [
+                  {
+                    "customSignal": {
+                      "customSignalOperator": "NUMERIC_LESS_THAN",
+                      "customSignalKey": "users",
+                      "targetCustomSignalValues": [
+                        "100"
+                      ]
+                    }
+                  }
+                ]
+              }
+            }
+          ]
+        }
+      }
+    },
+    {
+      "name": "chained_conditions",
+      "condition": {
+        "orCondition": {
+          "conditions": [
+            {
+              "andCondition": {
+                "conditions": [
+                  {
+                    "customSignal": {
+                      "customSignalOperator": "NUMERIC_LESS_THAN",
+                      "customSignalKey": "users",
+                      "targetCustomSignalValues": [
+                        "100"
+                      ]
+                    }
+                  },
+                  {
+                    "customSignal": {
+                      "customSignalOperator": "NUMERIC_GREATER_THAN",
+                      "customSignalKey": "premium users",
+                      "targetCustomSignalValues": [
+                        "20"
+                      ]
+                    }
+                  }
+                ]
+              }
+            }
+          ]
+        }
+      }
+    }
+  ],
+  "parameters": {
+    "Custom": {
+      "defaultValue": {
+        "value": "Default value"
+      },
+      "conditionalValues": {
+        "custom_signal": {
+          "value": "Conditional value"
+        }
+      }
+    },
+    "Welcome Message": {
+      "defaultValue": {
+        "value": "Welcome"
+      }
+    },
+    "Unset default value": {
+      "defaultValue": {
+        "value": ""
+      }
+    },
+    "Chained conditions": {
+      "defaultValue": {
+        "value": "Default value"
+      },
+      "conditionalValues": {
+        "chained_conditions": {
+          "value": "Conditional value"
+        }
+      }
+    }
+  },
+  "version": {
+    "versionNumber": "27",
+    "isLegacy": true
+  }
+}