From 72e5382aa381571351969f4638f6efdac4a08032 Mon Sep 17 00:00:00 2001
From: Varun Rathore <varunrathore@google.com>
Date: Tue, 29 Oct 2024 00:44:53 +0530
Subject: [PATCH 01/15] Added implemenation of evaluate function

---
 firebase_admin/remote_config.py | 597 ++++++++++++++++++++++++++++++++
 requirements.txt                |   3 +-
 2 files changed, 599 insertions(+), 1 deletion(-)
 create mode 100644 firebase_admin/remote_config.py

diff --git a/firebase_admin/remote_config.py b/firebase_admin/remote_config.py
new file mode 100644
index 000000000..576d6e36f
--- /dev/null
+++ b/firebase_admin/remote_config.py
@@ -0,0 +1,597 @@
+# Copyright 2017 Google Inc.
+#
+# 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.
+"""
+Firebase Remote Config Module.
+This module has required APIs
+ for the clients to use Firebase Remote Config with python.
+"""
+
+import logging
+from enum import Enum
+from typing import Dict, Optional, Literal, List, Callable, Any, Union
+import re
+import farmhash
+from firebase_admin import _http_client
+
+# Set up logging (you can customize the level and output)
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+
+ValueSource = Literal['default', 'remote', 'static']  # Define the ValueSource type
+
+MAX_CONDITION_RECURSION_DEPTH = 10
+
+class PercentConditionOperator(Enum):
+    """
+    Enum representing the available operators for percent conditions.
+    """
+    LESS_OR_EQUAL = "LESS_OR_EQUAL"
+    GREATER_THAN = "GREATER_THAN"
+    BETWEEN = "BETWEEN"
+    UNKNOWN = "UNKNOWN"
+
+
+class CustomSignalOperator(Enum):
+    """
+    Enum representing the available operators for custom signal conditions.
+    """
+    STRING_CONTAINS = "STRING_CONTAINS"
+    STRING_DOES_NOT_CONTAIN = "STRING_DOES_NOT_CONTAIN"
+    STRING_EXACTLY_MATCHES = "STRING_EXACTLY_MATCHES"
+    STRING_CONTAINS_REGEX = "STRING_CONTAINS_REGEX"
+    NUMERIC_LESS_THAN = "NUMERIC_LESS_THAN"
+    NUMERIC_LESS_EQUAL = "NUMERIC_LESS_EQUAL"
+    NUMERIC_EQUAL = "NUMERIC_EQUAL"
+    NUMERIC_NOT_EQUAL = "NUMERIC_NOT_EQUAL"
+    NUMERIC_GREATER_THAN = "NUMERIC_GREATER_THAN"
+    NUMERIC_GREATER_EQUAL = "NUMERIC_GREATER_EQUAL"
+    SEMANTIC_VERSION_LESS_THAN = "SEMANTIC_VERSION_LESS_THAN"
+    SEMANTIC_VERSION_LESS_EQUAL = "SEMANTIC_VERSION_LESS_EQUAL"
+    SEMANTIC_VERSION_EQUAL = "SEMANTIC_VERSION_EQUAL"
+    SEMANTIC_VERSION_NOT_EQUAL = "SEMANTIC_VERSION_NOT_EQUAL"
+    SEMANTIC_VERSION_GREATER_THAN = "SEMANTIC_VERSION_GREATER_THAN"
+    SEMANTIC_VERSION_GREATER_EQUAL = "SEMANTIC_VERSION_GREATER_EQUAL"
+
+class Condition:
+    """
+    Base class for conditions.
+    """
+    def __init__(self):
+        # This is just a base class, so it doesn't need any attributes
+        pass
+
+
+class OrCondition(Condition):
+    """
+    Represents an OR condition.
+    """
+    def __init__(self, conditions: List[Condition]):
+        super().__init__()
+        self.conditions = conditions
+
+
+class AndCondition(Condition):
+    """
+    Represents an AND condition.
+    """
+    def __init__(self, conditions: List[Condition]):
+        super().__init__()
+        self.conditions = conditions
+
+
+class PercentCondition(Condition):
+    """
+    Represents a percent condition.
+    """
+    def __init__(self, seed: str, percent_operator: PercentConditionOperator, micro_percent: int, micro_percent_range: Optional[Dict[str, int]] = None):
+        super().__init__()
+        self.seed = seed
+        self.percent_operator = percent_operator
+        self.micro_percent = micro_percent
+        self.micro_percent_range = micro_percent_range
+
+
+class CustomSignalCondition(Condition):
+    """
+    Represents a custom signal condition.
+    """
+    def __init__(self, custom_signal_operator: CustomSignalOperator, custom_signal_key: str, target_custom_signal_values: List[Union[str, int, float]]):
+        super().__init__()
+        self.custom_signal_operator = custom_signal_operator
+        self.custom_signal_key = custom_signal_key
+        self.target_custom_signal_values = target_custom_signal_values
+
+
+class OneOfCondition(Condition):
+    """
+    Represents a condition that can be one of several types.
+    """
+    def __init__(self, or_condition: Optional[OrCondition] = None, and_condition: Optional[AndCondition] = None, true_condition: Optional[bool] = None, false_condition: Optional[bool] = None, percent_condition: Optional[PercentCondition] = None, custom_signal_condition: Optional[CustomSignalCondition] = None):
+        super().__init__()
+        self.or_condition = or_condition
+        self.and_condition = and_condition
+        self.true_condition = true_condition
+        self.false_condition = false_condition
+        self.percent_condition = percent_condition
+        self.custom_signal_condition = custom_signal_condition
+
+
+class NamedCondition:
+    """
+    Represents a named condition.
+    """
+    def __init__(self, name: str, condition: OneOfCondition):
+        self.name = name
+        self.condition = condition
+
+
+class EvaluationContext:
+    """
+    Represents the context for evaluating conditions.
+    """
+    def __init__(self, **kwargs):
+        # This allows you to pass any key-value pairs to the context
+        # For example: EvaluationContext(user_country="US", user_type="paid")
+        self.__dict__.update(kwargs)
+
+    def __getattr__(self, item):
+        # This handles the case where a key is not found in the context
+        return None
+
+class ConditionEvaluator:
+    """
+    Encapsulates condition evaluation logic to simplify organization and
+    facilitate testing.
+    """
+
+    def evaluate_conditions(self, named_conditions: List['NamedCondition'], context: 'EvaluationContext') -> Dict[str, bool]:
+        """
+        Evaluates a list of named conditions and returns a dictionary of results.
+
+        Args:
+          named_conditions: A list of NamedCondition objects.
+          context: An EvaluationContext object.
+
+        Returns:
+          A dictionary mapping condition names to boolean evaluation results.
+        """
+        evaluated_conditions = {}
+        for named_condition in named_conditions:
+            evaluated_conditions[named_condition.name] = self.evaluate_condition(
+                named_condition.condition, context)
+        return evaluated_conditions
+
+    def evaluate_condition(self, condition: 'OneOfCondition', context: 'EvaluationContext', nesting_level: int = 0) -> bool:
+        """
+        Recursively evaluates a condition.
+
+        Args:
+          condition: The condition to evaluate.
+          context: An EvaluationContext object.
+          nesting_level: The current recursion depth.
+
+        Returns:
+          The boolean result of the condition evaluation.
+        """
+        if nesting_level >= MAX_CONDITION_RECURSION_DEPTH:
+            logger.warning("Maximum condition recursion depth exceeded.")
+            return False
+
+        if condition.or_condition:
+            return self.evaluate_or_condition(condition.or_condition, context, nesting_level + 1)
+        if condition.and_condition:
+            return self.evaluate_and_condition(condition.and_condition, context, nesting_level + 1)
+        if condition.true_condition:
+            return True
+        if condition.false_condition:
+            return False
+        if condition.percent_condition:
+            return self.evaluate_percent_condition(condition.percent_condition, context)
+        if condition.custom_signal_condition:
+            return self.evaluate_custom_signal_condition(condition.custom_signal_condition, context)
+        
+        logger.warning("Unknown condition type encountered.")
+        return False
+
+    def evaluate_or_condition(self, or_condition: 'OrCondition', context: 'EvaluationContext', nesting_level: int) -> bool:
+        """
+        Evaluates an OR condition.
+
+        Args:
+          or_condition: The OR condition to evaluate.
+          context: An EvaluationContext object.
+          nesting_level: The current recursion depth.
+
+        Returns:
+          True if any of the subconditions are true, False otherwise.
+        """
+        sub_conditions = or_condition.conditions or []
+        for sub_condition in sub_conditions:
+            result = self.evaluate_condition(sub_condition, context, nesting_level + 1)
+            if result:
+                return True
+        return False
+
+    def evaluate_and_condition(self, and_condition: 'AndCondition', context: 'EvaluationContext', nesting_level: int) -> bool:
+        """
+        Evaluates an AND condition.
+
+        Args:
+          and_condition: The AND condition to evaluate.
+          context: An EvaluationContext object.
+          nesting_level: The current recursion depth.
+
+        Returns:
+          True if all of the subconditions are true, False otherwise.
+        """
+        sub_conditions = and_condition.conditions or []
+        for sub_condition in sub_conditions:
+            result = self.evaluate_condition(sub_condition, context, nesting_level + 1)
+            if not result:
+                return False
+        return True
+
+    def evaluate_percent_condition(self, percent_condition: 'PercentCondition', context: 'EvaluationContext') -> bool:
+        """
+        Evaluates a percent condition.
+
+        Args:
+          percent_condition: The percent condition to evaluate.
+          context: An EvaluationContext object.
+
+        Returns:
+          True if the condition is met, False otherwise.
+        """
+        if not context.randomization_id:
+            logger.warning("Missing randomization ID for percent condition.")
+            return False
+
+        seed = percent_condition.seed
+        percent_operator = percent_condition.percent_operator 
+        micro_percent = percent_condition.micro_percent or 0
+        micro_percent_range = percent_condition.micro_percent_range
+
+        if not percent_operator:
+            logger.warning("Missing percent operator for percent condition.")
+            return False
+
+        normalized_micro_percent_upper_bound = micro_percent_range.micro_percent_upper_bound if micro_percent_range else 0
+        normalized_micro_percent_lower_bound = micro_percent_range.micro_percent_lower_bound if micro_percent_range else 0
+
+        seed_prefix = f"{seed}." if seed else ""
+        string_to_hash = f"{seed_prefix}{context.randomization_id}"
+
+        hash64 = ConditionEvaluator.hash_seeded_randomization_id(string_to_hash)
+
+        instance_micro_percentile = hash64 % (100 * 1_000_000)
+
+        if percent_operator == "LESS_OR_EQUAL":
+            return instance_micro_percentile <= micro_percent
+        elif percent_operator == "GREATER_THAN":
+            return instance_micro_percentile > micro_percent
+        elif percent_operator == "BETWEEN":
+            return normalized_micro_percent_lower_bound < instance_micro_percentile <= normalized_micro_percent_upper_bound
+        else:
+            logger.warning("Unknown percent operator: %s", percent_operator)
+            return False
+
+    @staticmethod
+    def hash_seeded_randomization_id(seeded_randomization_id: str) -> int:
+        """
+        Hashes a seeded randomization ID.
+
+        Args:
+          seeded_randomization_id: The seeded randomization ID to hash.
+
+        Returns:
+          The hashed value.
+        """
+        hash64 = farmhash.fingerprint64(seeded_randomization_id)
+        return abs(hash64)
+
+    def evaluate_custom_signal_condition(self, custom_signal_condition: 'CustomSignalCondition', context: 'EvaluationContext') -> bool:
+        """
+        Evaluates a custom signal condition.
+
+        Args:
+          custom_signal_condition: The custom signal condition to evaluate.
+          context: An EvaluationContext object.
+
+        Returns:
+          True if the condition is met, False otherwise.
+        """
+        custom_signal_operator = custom_signal_condition.custom_signal_operator
+        custom_signal_key = custom_signal_condition.custom_signal_key
+        target_custom_signal_values = custom_signal_condition.target_custom_signal_values
+
+        if not all([custom_signal_operator, custom_signal_key, target_custom_signal_values]):
+            logger.warning("Missing operator, key, or target values for custom signal condition.")
+            return False
+
+        if not target_custom_signal_values:
+            return False
+
+        actual_custom_signal_value = getattr(context, custom_signal_key, None)
+
+        if actual_custom_signal_value is None:
+            logger.warning("Custom signal value not found in context: %s", custom_signal_key)
+            return False
+        if custom_signal_operator == CustomSignalOperator.STRING_CONTAINS:
+            return compare_strings(lambda target, actual: target in actual)
+        elif custom_signal_operator == CustomSignalOperator.STRING_DOES_NOT_CONTAIN:
+            return not compare_strings(lambda target, actual: target in actual)
+        elif custom_signal_operator == CustomSignalOperator.STRING_EXACTLY_MATCHES:
+            return compare_strings(lambda target, actual: target.strip() == actual.strip())
+        elif custom_signal_operator == CustomSignalOperator.STRING_CONTAINS_REGEX:
+            return compare_strings(lambda target, actual: re.search(target, actual) is not None)
+        elif custom_signal_operator == CustomSignalOperator.NUMERIC_LESS_THAN:
+            return compare_numbers(lambda r: r < 0)
+        elif custom_signal_operator == CustomSignalOperator.NUMERIC_LESS_EQUAL:
+            return compare_numbers(lambda r: r <= 0)
+        elif custom_signal_operator == CustomSignalOperator.NUMERIC_EQUAL:
+            return compare_numbers(lambda r: r == 0)
+        elif custom_signal_operator == CustomSignalOperator.NUMERIC_NOT_EQUAL:
+            return compare_numbers(lambda r: r != 0)
+        elif custom_signal_operator == CustomSignalOperator.NUMERIC_GREATER_THAN:
+            return compare_numbers(lambda r: r > 0)
+        elif custom_signal_operator == CustomSignalOperator.NUMERIC_GREATER_EQUAL:
+            return compare_numbers(lambda r: r >= 0)
+        elif custom_signal_operator == CustomSignalOperator.SEMANTIC_VERSION_LESS_THAN:
+            return compare_semantic_versions(lambda r: r < 0)
+        elif custom_signal_operator == CustomSignalOperator.SEMANTIC_VERSION_LESS_EQUAL:
+            return compare_semantic_versions(lambda r: r <= 0)
+        elif custom_signal_operator == CustomSignalOperator.SEMANTIC_VERSION_EQUAL:
+            return compare_semantic_versions(lambda r: r == 0)
+        elif custom_signal_operator == CustomSignalOperator.SEMANTIC_VERSION_NOT_EQUAL:
+            return compare_semantic_versions(lambda r: r != 0)
+        elif custom_signal_operator == CustomSignalOperator.SEMANTIC_VERSION_GREATER_THAN:
+            return compare_semantic_versions(lambda r: r > 0)
+        elif custom_signal_operator == CustomSignalOperator.SEMANTIC_VERSION_GREATER_EQUAL:
+            return compare_semantic_versions(lambda r: r >= 0)
+
+        logger.warning("Unknown custom signal operator: %s", custom_signal_operator)
+        return False
+
+        def compare_strings(predicate_fn: Callable[[str, str], bool]) -> bool:
+            return any(predicate_fn(target, str(actual_custom_signal_value)) for target in target_custom_signal_values)
+
+        def compare_numbers(predicate_fn: Callable[[int], bool]) -> bool:
+            try:
+                target = float(target_custom_signal_values[0])
+                actual = float(actual_custom_signal_value)
+                result = -1 if actual < target else 1 if actual > target else 0
+                return predicate_fn(result)
+            except ValueError:
+                logger.warning("Invalid numeric value for comparison.")
+                return False
+
+        def compare_semantic_versions(predicate_fn: Callable[[int], bool]) -> bool:
+            return compare_versions(str(actual_custom_signal_value), str(target_custom_signal_values[0]), predicate_fn)
+            
+        def compare_versions(version1: str, version2: str, predicate_fn: Callable[[int], bool]) -> bool:
+            """
+            Compares two semantic version strings.
+
+            Args:
+            version1: The first semantic version string.
+            version2: The second semantic version string.
+            predicate_fn: A function that takes an integer and returns a boolean.
+
+            Returns:
+            The result of the predicate function.
+            """
+            try:
+                v1_parts = [int(part) for part in version1.split('.')]
+                v2_parts = [int(part) for part in version2.split('.')]
+                max_length = max(len(v1_parts), len(v2_parts))
+                v1_parts.extend([0] * (max_length - len(v1_parts)))
+                v2_parts.extend([0] * (max_length - len(v2_parts)))
+
+                for part1, part2 in zip(v1_parts, v2_parts):
+                    if part1 < part2:
+                        return predicate_fn(-1)
+                    elif part1 > part2:
+                        return predicate_fn(1)
+                return predicate_fn(0) 
+
+            except ValueError:
+                logger.warning("Invalid semantic version format for comparison.")
+                return False
+
+
+class RemoteConfig:
+    """
+    Represents a Server 
+    Side Remote Config Class.
+    """
+
+    def __init__(self, app=None):
+        timeout = app.options.get('httpTimeout',
+                                  _http_client.DEFAULT_TIMEOUT_SECONDS)
+        self._credential = app.credential.get_credential()
+        self._api_client = _http_client.RemoteConfigApiClient(
+            credential=self._credential, timeout=timeout)
+
+    async def get_server_template(self, default_config: Optional[Dict[str, str]] = None):
+        template = self.init_server_template(default_config)
+        await template.load()
+        return template
+
+    def init_server_template(self, default_config: Optional[Dict[str, str]] = None):
+        template = ServerTemplate(self._api_client,
+                                  default_config=default_config)
+        return template
+
+class ServerTemplateData:
+    """Represents a Server Template Data class."""
+
+    def __init__(self, template: Dict[str, Any]):
+        self.conditions = template.get('conditions', [])
+        self.parameters = template.get('parameters', {})
+        # ... (Add any other necessary attributes from the template data) ...
+
+
+class ServerTemplate:
+    """Represents a Server Template with implementations for loading and evaluating the template."""
+
+    def __init__(self, client, default_config: Optional[Dict[str, str]] = None):
+        """
+        Initializes a ServerTemplate instance.
+
+        Args:
+          client: The API client used to fetch the server template.
+          default_config:  A dictionary of default configuration values.
+        """
+        self._client = client
+        self._condition_evaluator = ConditionEvaluator()
+        self._cache = None
+        self._stringified_default_config = {key: str(value) for key, value in default_config.items()} if default_config else {}
+
+    async def load(self):
+        """Fetches and caches the server template from the Remote Config API."""
+        self._cache = await self._client.get_server_template()
+
+    def set(self, template):
+        """
+        Sets the server template from a string or ServerTemplateData object.
+
+        Args:
+          template: The template to set, either as a JSON string or a ServerTemplateData object.
+        """
+        if isinstance(template, str):
+            try:
+                import json
+                parsed_template = json.loads(template)
+                self._cache = ServerTemplateData(parsed_template)
+            except json.JSONDecodeError as e:
+                raise ValueError(f"Failed to parse the JSON string: {template}. {e}")
+        elif isinstance(template, ServerTemplateData):
+            self._cache = template
+        else:
+            raise TypeError("template must be a string or ServerTemplateData object")
+
+    def evaluate(self, context: Optional[Dict[str, Union[str, int]]] = None) -> 'ServerConfig':
+        """
+        Evaluates the cached server template to produce a ServerConfig.
+
+        Args:
+          context: A dictionary of values to use for evaluating conditions.
+
+        Returns:
+          A ServerConfig object.
+        """
+        if not self._cache:
+            raise ValueError("No Remote Config Server template in cache. Call load() before calling evaluate().")
+
+        context = context or {}
+        evaluated_conditions = self._condition_evaluator.evaluate_conditions(
+            self._cache.conditions, EvaluationContext(**context)
+        )
+
+        config_values = {}
+
+        for key, value in self._stringified_default_config.items():
+            config_values[key] = Value('default', value)
+
+        for key, parameter in self._cache.parameters.items():
+            conditional_values = parameter.get('conditionalValues', {})
+            default_value = parameter.get('defaultValue')
+
+            parameter_value_wrapper = None
+            for condition_name, condition_evaluation in evaluated_conditions.items():
+                if condition_name in conditional_values and condition_evaluation:
+                    parameter_value_wrapper = conditional_values[condition_name]
+                    break
+
+            if parameter_value_wrapper and parameter_value_wrapper.get('useInAppDefault'):
+                logger.info("Using in-app default value for key '%s'", key)
+                continue
+
+            if parameter_value_wrapper:
+                config_values[key] = Value('remote', parameter_value_wrapper.get('value'))
+                continue
+
+            if not default_value:
+                logger.warning("No default value found for key '%s'", key)
+                continue
+
+            if default_value.get('useInAppDefault'):
+                logger.info("Using in-app default value for key '%s'", key)
+                continue
+
+            config_values[key] = Value('remote', default_value.get('value'))
+
+        return ServerConfig(config_values)
+
+
+class ServerConfig:
+    """Represents a Remote Config Server Side Config."""
+
+    def __init__(self, config_values):
+        self._config_values = config_values
+
+    def get_boolean(self, key):
+        return self._config_values[key].as_boolean()
+
+    def get_string(self, key):
+        return self._config_values[key].as_string()
+
+    def get_int(self, key):
+        return int(self._config_values[key].as_number())
+
+    def get_value(self, key):
+        return self._config_values[key]
+
+class Value:
+    """
+    Represents a value fetched from Remote Config.
+    """
+    DEFAULT_VALUE_FOR_BOOLEAN = False
+    DEFAULT_VALUE_FOR_STRING = ''
+    DEFAULT_VALUE_FOR_NUMBER = 0
+    BOOLEAN_TRUTHY_VALUES = ['1', 'true', 't', 'yes', 'y', 'on']
+
+    def __init__(self, source: ValueSource, value: str = DEFAULT_VALUE_FOR_STRING):
+        """
+        Initializes a Value instance.
+
+        Args:
+          source: The source of the value (e.g., 'default', 'remote', 'static').
+          value: The string value.
+        """
+        self.source = source
+        self.value = value
+
+    def as_string(self) -> str:
+        """Returns the value as a string."""
+        return self.value
+
+    def as_boolean(self) -> bool:
+        """Returns the value as a boolean."""
+        if self.source == 'static':
+            return self.DEFAULT_VALUE_FOR_BOOLEAN
+        return self.value.lower() in self.BOOLEAN_TRUTHY_VALUES
+
+    def as_number(self) -> float:
+        """Returns the value as a number."""
+        if self.source == 'static':
+            return self.DEFAULT_VALUE_FOR_NUMBER
+        try:
+            return float(self.value)
+        except ValueError:
+            return self.DEFAULT_VALUE_FOR_NUMBER
+
+    def get_source(self) -> ValueSource:
+        """Returns the source of the value."""
+        return self.source
diff --git a/requirements.txt b/requirements.txt
index acf09438b..c726b758a 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -11,4 +11,5 @@ google-api-core[grpc] >= 1.22.1, < 3.0.0dev; platform.python_implementation != '
 google-api-python-client >= 1.7.8
 google-cloud-firestore >= 2.9.1; platform.python_implementation != 'PyPy'
 google-cloud-storage >= 1.37.1
-pyjwt[crypto] >= 2.5.0
\ No newline at end of file
+pyjwt[crypto] >= 2.5.0
+pyfarmhash >= 0.4.0
\ No newline at end of file

From 94afdbee762261a6fc33b7362339141adda2fe82 Mon Sep 17 00:00:00 2001
From: Varun Rathore <varunrathore@google.com>
Date: Wed, 30 Oct 2024 18:32:36 +0530
Subject: [PATCH 02/15] Improvement

---
 firebase_admin/remote_config.py | 619 ++++++++++++++++----------------
 1 file changed, 312 insertions(+), 307 deletions(-)

diff --git a/firebase_admin/remote_config.py b/firebase_admin/remote_config.py
index 576d6e36f..4411db4de 100644
--- a/firebase_admin/remote_config.py
+++ b/firebase_admin/remote_config.py
@@ -11,40 +11,30 @@
 # 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.
-"""
-Firebase Remote Config Module.
-This module has required APIs
- for the clients to use Firebase Remote Config with python.
+
+"""Firebase Remote Config Module.
+This module has required APIs for the clients to use Firebase Remote Config with python.
 """
 
+import json
 import logging
+from typing import Dict, Optional, Literal, Callable, Union
 from enum import Enum
-from typing import Dict, Optional, Literal, List, Callable, Any, Union
 import re
 import farmhash
-from firebase_admin import _http_client
+from firebase_admin import App, _http_client, _utils
+import firebase_admin
 
 # Set up logging (you can customize the level and output)
 logging.basicConfig(level=logging.INFO)
 logger = logging.getLogger(__name__)
 
-ValueSource = Literal['default', 'remote', 'static']  # Define the ValueSource type
-
+_REMOTE_CONFIG_ATTRIBUTE = '_remoteconfig'
 MAX_CONDITION_RECURSION_DEPTH = 10
-
-class PercentConditionOperator(Enum):
-    """
-    Enum representing the available operators for percent conditions.
-    """
-    LESS_OR_EQUAL = "LESS_OR_EQUAL"
-    GREATER_THAN = "GREATER_THAN"
-    BETWEEN = "BETWEEN"
-    UNKNOWN = "UNKNOWN"
-
+ValueSource = Literal['default', 'remote', 'static']  # Define the ValueSource type
 
 class CustomSignalOperator(Enum):
-    """
-    Enum representing the available operators for custom signal conditions.
+    """Enum representing the available operators for custom signal conditions.
     """
     STRING_CONTAINS = "STRING_CONTAINS"
     STRING_DOES_NOT_CONTAIN = "STRING_DOES_NOT_CONTAIN"
@@ -63,101 +53,207 @@ class CustomSignalOperator(Enum):
     SEMANTIC_VERSION_GREATER_THAN = "SEMANTIC_VERSION_GREATER_THAN"
     SEMANTIC_VERSION_GREATER_EQUAL = "SEMANTIC_VERSION_GREATER_EQUAL"
 
-class Condition:
-    """
-    Base class for conditions.
-    """
-    def __init__(self):
-        # This is just a base class, so it doesn't need any attributes
-        pass
+class ServerTemplateData:
+    """Represents a Server Template Data class."""
+    def __init__(self, etag, template_data):
+        """Initializes a new ServerTemplateData instance.
 
+        Args:
+            etag: The string to be used for initialize the ETag property.
+            template_data: The data to be parsed for getting the parameters and conditions.
+        """
+        self._parameters = template_data['parameters']
+        self._conditions = template_data['conditions']
+        self._version = template_data['version']
+        self._parameter_groups = template_data['parameterGroups']
+        self._etag = etag
 
-class OrCondition(Condition):
-    """
-    Represents an OR condition.
-    """
-    def __init__(self, conditions: List[Condition]):
-        super().__init__()
-        self.conditions = conditions
+    @property
+    def parameters(self):
+        return self._parameters
 
+    @property
+    def etag(self):
+        return self._etag
 
-class AndCondition(Condition):
-    """
-    Represents an AND condition.
-    """
-    def __init__(self, conditions: List[Condition]):
-        super().__init__()
-        self.conditions = conditions
+    @property
+    def version(self):
+        return self._version
 
+    @property
+    def conditions(self):
+        return self._conditions
 
-class PercentCondition(Condition):
-    """
-    Represents a percent condition.
-    """
-    def __init__(self, seed: str, percent_operator: PercentConditionOperator, micro_percent: int, micro_percent_range: Optional[Dict[str, int]] = None):
-        super().__init__()
-        self.seed = seed
-        self.percent_operator = percent_operator
-        self.micro_percent = micro_percent
-        self.micro_percent_range = micro_percent_range
+    @property
+    def parameter_groups(self):
+        return self._parameter_groups
 
 
-class CustomSignalCondition(Condition):
-    """
-    Represents a custom signal condition.
-    """
-    def __init__(self, custom_signal_operator: CustomSignalOperator, custom_signal_key: str, target_custom_signal_values: List[Union[str, int, float]]):
-        super().__init__()
-        self.custom_signal_operator = custom_signal_operator
-        self.custom_signal_key = custom_signal_key
-        self.target_custom_signal_values = target_custom_signal_values
+class ServerTemplate:
+    """Represents a Server Template with implementations for loading and evaluting the tempalte."""
+    def __init__(self, app: App = None, default_config: Optional[Dict[str, str]] = None):
+        """Initializes a ServerTemplate instance.
+
+        Args:
+          app: App instance to be used. This is optional and the default app instance will
+                be used if not present.
+          default_config: The default config to be used in the evaluated config.
+        """
+        self._rc_service = _utils.get_app_service(app,
+                                                  _REMOTE_CONFIG_ATTRIBUTE, _RemoteConfigService)
+        # This gets set when the template is
+        # fetched from RC servers via the load API, or via the set API.
+        self._cache = None
+        if default_config is not None:
+            self._stringified_default_config = json.dumps(default_config)
+        else:
+            self._stringified_default_config = None
 
+    async def load(self):
+        """Fetches the server template and caches the data."""
+        self._cache = await self._rc_service.getServerTemplate()
 
-class OneOfCondition(Condition):
-    """
-    Represents a condition that can be one of several types.
-    """
-    def __init__(self, or_condition: Optional[OrCondition] = None, and_condition: Optional[AndCondition] = None, true_condition: Optional[bool] = None, false_condition: Optional[bool] = None, percent_condition: Optional[PercentCondition] = None, custom_signal_condition: Optional[CustomSignalCondition] = None):
-        super().__init__()
-        self.or_condition = or_condition
-        self.and_condition = and_condition
-        self.true_condition = true_condition
-        self.false_condition = false_condition
-        self.percent_condition = percent_condition
-        self.custom_signal_condition = custom_signal_condition
+    def evaluate(self, context: Optional[Dict[str, Union[str, int]]] = None) -> 'ServerConfig':
+        """Evaluates the cached server template to produce a ServerConfig.
 
+        Args:
+          context: A dictionary of values to use for evaluating conditions.
 
-class NamedCondition:
-    """
-    Represents a named condition.
-    """
-    def __init__(self, name: str, condition: OneOfCondition):
-        self.name = name
-        self.condition = condition
+        Returns:
+          A ServerConfig object.
+        Raises:
+            ValueError: If the input arguments are invalid.
+        """
+        # Logic to process the cached template into a ServerConfig here.
+        # TODO: Add Condition evaluator.
+        if not self._cache:
+            raise ValueError("""No Remote Config Server template in cache.
+                            Call load() before calling evaluate().""")
+        context = context or {}
+        config_values = {}
 
+        # Initializes config Value objects with default values.
+        for key, value in self._stringified_default_config.items():
+            config_values[key] = _Value('default', value)
 
-class EvaluationContext:
-    """
-    Represents the context for evaluating conditions.
-    """
-    def __init__(self, **kwargs):
-        # This allows you to pass any key-value pairs to the context
-        # For example: EvaluationContext(user_country="US", user_type="paid")
-        self.__dict__.update(kwargs)
+        self._evaluator = _ConditionEvaluator(self._cache.conditions, context,
+                                              config_values, self._cache.parameters)
+        return ServerConfig(config_values=self._evaluator.evaluate())
 
-    def __getattr__(self, item):
-        # This handles the case where a key is not found in the context
-        return None
+    def set(self, template):
+        """Updates the cache to store the given template is of type ServerTemplateData.
 
-class ConditionEvaluator:
-    """
-    Encapsulates condition evaluation logic to simplify organization and
-    facilitate testing.
+        Args:
+          template: An object of type ServerTemplateData to be cached.
+        """
+        if isinstance(template, ServerTemplateData):
+            self._cache = template
+
+
+class ServerConfig:
+    """Represents a Remote Config Server Side Config."""
+    def __init__(self, config_values):
+        self._config_values = config_values # dictionary of param key to values
+
+    def get_boolean(self, key):
+        return bool(self.get_value(key))
+
+    def get_string(self, key):
+        return str(self.get_value(key))
+
+    def get_int(self, key):
+        return int(self.get_value(key))
+
+    def get_value(self, key):
+        return self._config_values[key]
+
+
+class _RemoteConfigService:
+    """Internal class that facilitates sending requests to the Firebase Remote
+        Config backend API.
     """
+    def __init__(self, app):
+        """Initialize a JsonHttpClient with necessary inputs.
 
-    def evaluate_conditions(self, named_conditions: List['NamedCondition'], context: 'EvaluationContext') -> Dict[str, bool]:
+        Args:
+            app: App instance to be used for fetching app specific details required
+                for initializing the http client.
         """
-        Evaluates a list of named conditions and returns a dictionary of results.
+        remote_config_base_url = 'https://firebaseremoteconfig.googleapis.com'
+        self._project_id = app.project_id
+        app_credential = app.credential.get_credential()
+        rc_headers = {
+            'X-FIREBASE-CLIENT': 'fire-admin-python/{0}'.format(firebase_admin.__version__), }
+        timeout = app.options.get('httpTimeout', _http_client.DEFAULT_TIMEOUT_SECONDS)
+
+        self._client = _http_client.JsonHttpClient(credential=app_credential,
+                                                   base_url=remote_config_base_url,
+                                                   headers=rc_headers, timeout=timeout)
+
+
+    def get_server_template(self):
+        """Requests for a server template and converts the response to an instance of
+        ServerTemplateData for storing the template parameters and conditions."""
+        url_prefix = self._get_url_prefix()
+        headers, response_json = self._client.headers_and_body('get',
+                                                               url=url_prefix+'/namespaces/ \
+                                                               firebase-server/serverRemoteConfig')
+        return ServerTemplateData(headers.get('ETag'), response_json)
+
+    def _get_url_prefix(self):
+        # Returns project prefix for url, in the format of
+        # /v1/projects/${projectId}
+        return "/v1/projects/{0}".format(self._project_id)
+
+
+class _ConditionEvaluator:
+    """Internal class that facilitates sending requests to the Firebase Remote
+    Config backend API."""
+    def __init__(self, context, conditions, config_values, parameters):
+        self._context = context
+        self._conditions = conditions
+        self._parameters = parameters
+        self._config_values = config_values
+
+    def evaluate(self):
+        """Internal function Evaluates the cached server template to produce
+        a ServerConfig"""
+        evaluated_conditions = self.evaluate_conditions(self._conditions, self._context)
+
+        # Overlays config Value objects derived by evaluating the template.
+        for key, parameter in self._parameters.items():
+            conditional_values = parameter.conditional_values or {}
+            default_value = parameter.default_value or {}
+            parameter_value_wrapper = None
+
+            # Iterates in order over condition list. If there is a value associated
+            # with a condition, this checks if the condition is true.
+            for condition_name, condition_evaluation in evaluated_conditions.items():
+                if condition_name in conditional_values and condition_evaluation:
+                    parameter_value_wrapper = conditional_values[condition_name]
+                    break
+            if parameter_value_wrapper and parameter_value_wrapper.get('useInAppDefault'):
+                logger.info("Using in-app default value for key '%s'", key)
+                continue
+
+            if parameter_value_wrapper:
+                parameter_value = parameter_value_wrapper.value
+                self._config_values[key] = _Value('remote', parameter_value)
+                continue
+
+            if not default_value:
+                logger.warning("No default value found for key '%s'", key)
+                continue
+
+            if default_value.get('useInAppDefault'):
+                logger.info("Using in-app default value for key '%s'", key)
+                continue
+
+            self._config_values[key] = _Value('remote', default_value.get('value'))
+        return self._config_values
+
+    def evaluate_conditions(self, named_conditions, context)-> Dict[str, bool]:
+        """Evaluates a list of named conditions and returns a dictionary of results.
 
         Args:
           named_conditions: A list of NamedCondition objects.
@@ -169,12 +265,13 @@ def evaluate_conditions(self, named_conditions: List['NamedCondition'], context:
         evaluated_conditions = {}
         for named_condition in named_conditions:
             evaluated_conditions[named_condition.name] = self.evaluate_condition(
-                named_condition.condition, context)
+                named_condition.condition, context
+            )
         return evaluated_conditions
 
-    def evaluate_condition(self, condition: 'OneOfCondition', context: 'EvaluationContext', nesting_level: int = 0) -> bool:
-        """
-        Recursively evaluates a condition.
+    def evaluate_condition(self, condition, context,
+                           nesting_level: int = 0) -> bool:
+        """Recursively evaluates a condition.
 
         Args:
           condition: The condition to evaluate.
@@ -187,7 +284,6 @@ def evaluate_condition(self, condition: 'OneOfCondition', context: 'EvaluationCo
         if nesting_level >= MAX_CONDITION_RECURSION_DEPTH:
             logger.warning("Maximum condition recursion depth exceeded.")
             return False
-
         if condition.or_condition:
             return self.evaluate_or_condition(condition.or_condition, context, nesting_level + 1)
         if condition.and_condition:
@@ -200,13 +296,13 @@ def evaluate_condition(self, condition: 'OneOfCondition', context: 'EvaluationCo
             return self.evaluate_percent_condition(condition.percent_condition, context)
         if condition.custom_signal_condition:
             return self.evaluate_custom_signal_condition(condition.custom_signal_condition, context)
-        
         logger.warning("Unknown condition type encountered.")
         return False
 
-    def evaluate_or_condition(self, or_condition: 'OrCondition', context: 'EvaluationContext', nesting_level: int) -> bool:
-        """
-        Evaluates an OR condition.
+    def evaluate_or_condition(self, or_condition,
+                              context,
+                              nesting_level: int = 0) -> bool:
+        """Evaluates an OR condition.
 
         Args:
           or_condition: The OR condition to evaluate.
@@ -223,9 +319,10 @@ def evaluate_or_condition(self, or_condition: 'OrCondition', context: 'Evaluatio
                 return True
         return False
 
-    def evaluate_and_condition(self, and_condition: 'AndCondition', context: 'EvaluationContext', nesting_level: int) -> bool:
-        """
-        Evaluates an AND condition.
+    def evaluate_and_condition(self, and_condition,
+                               context,
+                               nesting_level: int = 0) -> bool:
+        """Evaluates an AND condition.
 
         Args:
           and_condition: The AND condition to evaluate.
@@ -242,9 +339,9 @@ def evaluate_and_condition(self, and_condition: 'AndCondition', context: 'Evalua
                 return False
         return True
 
-    def evaluate_percent_condition(self, percent_condition: 'PercentCondition', context: 'EvaluationContext') -> bool:
-        """
-        Evaluates a percent condition.
+    def evaluate_percent_condition(self, percent_condition,
+                                   context) -> bool:
+        """Evaluates a percent condition.
 
         Args:
           percent_condition: The percent condition to evaluate.
@@ -258,38 +355,36 @@ def evaluate_percent_condition(self, percent_condition: 'PercentCondition', cont
             return False
 
         seed = percent_condition.seed
-        percent_operator = percent_condition.percent_operator 
+        percent_operator = percent_condition.percent_operator
         micro_percent = percent_condition.micro_percent or 0
         micro_percent_range = percent_condition.micro_percent_range
 
         if not percent_operator:
             logger.warning("Missing percent operator for percent condition.")
             return False
-
-        normalized_micro_percent_upper_bound = micro_percent_range.micro_percent_upper_bound if micro_percent_range else 0
-        normalized_micro_percent_lower_bound = micro_percent_range.micro_percent_lower_bound if micro_percent_range else 0
-
+        if micro_percent_range:
+            norm_percent_upper_bound = micro_percent_range.micro_percent_upper_bound
+            norm_percent_lower_bound = micro_percent_range.micro_percent_lower_bound
+        else:
+            norm_percent_upper_bound = 0
+            norm_percent_lower_bound = 0
         seed_prefix = f"{seed}." if seed else ""
         string_to_hash = f"{seed_prefix}{context.randomization_id}"
 
-        hash64 = ConditionEvaluator.hash_seeded_randomization_id(string_to_hash)
+        hash64 = self.hash_seeded_randomization_id(string_to_hash)
 
         instance_micro_percentile = hash64 % (100 * 1_000_000)
 
         if percent_operator == "LESS_OR_EQUAL":
             return instance_micro_percentile <= micro_percent
-        elif percent_operator == "GREATER_THAN":
+        if percent_operator == "GREATER_THAN":
             return instance_micro_percentile > micro_percent
-        elif percent_operator == "BETWEEN":
-            return normalized_micro_percent_lower_bound < instance_micro_percentile <= normalized_micro_percent_upper_bound
-        else:
-            logger.warning("Unknown percent operator: %s", percent_operator)
-            return False
-
-    @staticmethod
-    def hash_seeded_randomization_id(seeded_randomization_id: str) -> int:
-        """
-        Hashes a seeded randomization ID.
+        if percent_operator == "BETWEEN":
+            return norm_percent_lower_bound < instance_micro_percentile <= norm_percent_upper_bound
+        logger.warning("Unknown percent operator: %s", percent_operator)
+        return False
+    def hash_seeded_randomization_id(self, seeded_randomization_id: str) -> int:
+        """Hashes a seeded randomization ID.
 
         Args:
           seeded_randomization_id: The seeded randomization ID to hash.
@@ -297,12 +392,11 @@ def hash_seeded_randomization_id(seeded_randomization_id: str) -> int:
         Returns:
           The hashed value.
         """
-        hash64 = farmhash.fingerprint64(seeded_randomization_id)
+        hash64 = farmhash.hash64withseed(seeded_randomization_id)
         return abs(hash64)
-
-    def evaluate_custom_signal_condition(self, custom_signal_condition: 'CustomSignalCondition', context: 'EvaluationContext') -> bool:
-        """
-        Evaluates a custom signal condition.
+    def evaluate_custom_signal_condition(self, custom_signal_condition,
+                                         context) -> bool:
+        """Evaluates a custom signal condition.
 
         Args:
           custom_signal_condition: The custom signal condition to evaluate.
@@ -321,50 +415,59 @@ def evaluate_custom_signal_condition(self, custom_signal_condition: 'CustomSigna
 
         if not target_custom_signal_values:
             return False
-
         actual_custom_signal_value = getattr(context, custom_signal_key, None)
-
         if actual_custom_signal_value is None:
             logger.warning("Custom signal value not found in context: %s", custom_signal_key)
             return False
         if custom_signal_operator == CustomSignalOperator.STRING_CONTAINS:
             return compare_strings(lambda target, actual: target in actual)
-        elif custom_signal_operator == CustomSignalOperator.STRING_DOES_NOT_CONTAIN:
+        if custom_signal_operator == CustomSignalOperator.STRING_DOES_NOT_CONTAIN:
             return not compare_strings(lambda target, actual: target in actual)
-        elif custom_signal_operator == CustomSignalOperator.STRING_EXACTLY_MATCHES:
+        if custom_signal_operator == CustomSignalOperator.STRING_EXACTLY_MATCHES:
             return compare_strings(lambda target, actual: target.strip() == actual.strip())
-        elif custom_signal_operator == CustomSignalOperator.STRING_CONTAINS_REGEX:
+        if custom_signal_operator == CustomSignalOperator.STRING_CONTAINS_REGEX:
             return compare_strings(lambda target, actual: re.search(target, actual) is not None)
-        elif custom_signal_operator == CustomSignalOperator.NUMERIC_LESS_THAN:
+        if custom_signal_operator == CustomSignalOperator.NUMERIC_LESS_THAN:
             return compare_numbers(lambda r: r < 0)
-        elif custom_signal_operator == CustomSignalOperator.NUMERIC_LESS_EQUAL:
+        if custom_signal_operator == CustomSignalOperator.NUMERIC_LESS_EQUAL:
             return compare_numbers(lambda r: r <= 0)
-        elif custom_signal_operator == CustomSignalOperator.NUMERIC_EQUAL:
+        if custom_signal_operator == CustomSignalOperator.NUMERIC_EQUAL:
             return compare_numbers(lambda r: r == 0)
-        elif custom_signal_operator == CustomSignalOperator.NUMERIC_NOT_EQUAL:
+        if custom_signal_operator == CustomSignalOperator.NUMERIC_NOT_EQUAL:
             return compare_numbers(lambda r: r != 0)
-        elif custom_signal_operator == CustomSignalOperator.NUMERIC_GREATER_THAN:
+        if custom_signal_operator == CustomSignalOperator.NUMERIC_GREATER_THAN:
             return compare_numbers(lambda r: r > 0)
-        elif custom_signal_operator == CustomSignalOperator.NUMERIC_GREATER_EQUAL:
+        if custom_signal_operator == CustomSignalOperator.NUMERIC_GREATER_EQUAL:
             return compare_numbers(lambda r: r >= 0)
-        elif custom_signal_operator == CustomSignalOperator.SEMANTIC_VERSION_LESS_THAN:
+        if custom_signal_operator == CustomSignalOperator.SEMANTIC_VERSION_LESS_THAN:
             return compare_semantic_versions(lambda r: r < 0)
-        elif custom_signal_operator == CustomSignalOperator.SEMANTIC_VERSION_LESS_EQUAL:
+        if custom_signal_operator == CustomSignalOperator.SEMANTIC_VERSION_LESS_EQUAL:
             return compare_semantic_versions(lambda r: r <= 0)
-        elif custom_signal_operator == CustomSignalOperator.SEMANTIC_VERSION_EQUAL:
+        if custom_signal_operator == CustomSignalOperator.SEMANTIC_VERSION_EQUAL:
             return compare_semantic_versions(lambda r: r == 0)
-        elif custom_signal_operator == CustomSignalOperator.SEMANTIC_VERSION_NOT_EQUAL:
+        if custom_signal_operator == CustomSignalOperator.SEMANTIC_VERSION_NOT_EQUAL:
             return compare_semantic_versions(lambda r: r != 0)
-        elif custom_signal_operator == CustomSignalOperator.SEMANTIC_VERSION_GREATER_THAN:
+        if custom_signal_operator == CustomSignalOperator.SEMANTIC_VERSION_GREATER_THAN:
             return compare_semantic_versions(lambda r: r > 0)
-        elif custom_signal_operator == CustomSignalOperator.SEMANTIC_VERSION_GREATER_EQUAL:
+        if custom_signal_operator == CustomSignalOperator.SEMANTIC_VERSION_GREATER_EQUAL:
             return compare_semantic_versions(lambda r: r >= 0)
 
-        logger.warning("Unknown custom signal operator: %s", custom_signal_operator)
-        return False
-
         def compare_strings(predicate_fn: Callable[[str, str], bool]) -> bool:
-            return any(predicate_fn(target, str(actual_custom_signal_value)) for target in target_custom_signal_values)
+            """Compares the actual string value of a signal against a list of target values.
+
+            Args:
+                predicate_fn: A function that takes two string arguments (target and actual)
+                                and returns a boolean indicating whether
+                                the target matches the actual value.
+
+            Returns:
+                bool: True if the predicate function returns True for any target value in the list,
+                    False otherwise.
+            """
+            for target in target_custom_signal_values:
+                if predicate_fn(target, str(actual_custom_signal_value)):
+                    return True
+            return False
 
         def compare_numbers(predicate_fn: Callable[[int], bool]) -> bool:
             try:
@@ -377,11 +480,22 @@ def compare_numbers(predicate_fn: Callable[[int], bool]) -> bool:
                 return False
 
         def compare_semantic_versions(predicate_fn: Callable[[int], bool]) -> bool:
-            return compare_versions(str(actual_custom_signal_value), str(target_custom_signal_values[0]), predicate_fn)
-            
-        def compare_versions(version1: str, version2: str, predicate_fn: Callable[[int], bool]) -> bool:
+            """Compares the actual semantic version value of a signal against a target value.
+            Calls the predicate function with -1, 0, 1 if actual is less than, equal to,
+            or greater than target.
+
+            Args:
+            predicate_fn: A function that takes an integer (-1, 0, or 1) and returns a boolean.
+
+            Returns:
+                bool: True if the predicate function returns True for the result of the comparison,
+            False otherwise.
             """
-            Compares two semantic version strings.
+            return compare_versions(str(actual_custom_signal_value),
+                                    str(target_custom_signal_values[0]), predicate_fn)
+        def compare_versions(version1: str, version2: str,
+                             predicate_fn: Callable[[int], bool]) -> bool:
+            """Compares two semantic version strings.
 
             Args:
             version1: The first semantic version string.
@@ -389,7 +503,7 @@ def compare_versions(version1: str, version2: str, predicate_fn: Callable[[int],
             predicate_fn: A function that takes an integer and returns a boolean.
 
             Returns:
-            The result of the predicate function.
+                bool: The result of the predicate function.
             """
             try:
                 v1_parts = [int(part) for part in version1.split('.')]
@@ -401,161 +515,52 @@ def compare_versions(version1: str, version2: str, predicate_fn: Callable[[int],
                 for part1, part2 in zip(v1_parts, v2_parts):
                     if part1 < part2:
                         return predicate_fn(-1)
-                    elif part1 > part2:
+                    if part1 > part2:
                         return predicate_fn(1)
-                return predicate_fn(0) 
-
+                return predicate_fn(0)
             except ValueError:
                 logger.warning("Invalid semantic version format for comparison.")
                 return False
 
+        logger.warning("Unknown custom signal operator: %s", custom_signal_operator)
+        return False
 
-class RemoteConfig:
-    """
-    Represents a Server 
-    Side Remote Config Class.
-    """
-
-    def __init__(self, app=None):
-        timeout = app.options.get('httpTimeout',
-                                  _http_client.DEFAULT_TIMEOUT_SECONDS)
-        self._credential = app.credential.get_credential()
-        self._api_client = _http_client.RemoteConfigApiClient(
-            credential=self._credential, timeout=timeout)
-
-    async def get_server_template(self, default_config: Optional[Dict[str, str]] = None):
-        template = self.init_server_template(default_config)
-        await template.load()
-        return template
-
-    def init_server_template(self, default_config: Optional[Dict[str, str]] = None):
-        template = ServerTemplate(self._api_client,
-                                  default_config=default_config)
-        return template
-
-class ServerTemplateData:
-    """Represents a Server Template Data class."""
-
-    def __init__(self, template: Dict[str, Any]):
-        self.conditions = template.get('conditions', [])
-        self.parameters = template.get('parameters', {})
-        # ... (Add any other necessary attributes from the template data) ...
-
-
-class ServerTemplate:
-    """Represents a Server Template with implementations for loading and evaluating the template."""
-
-    def __init__(self, client, default_config: Optional[Dict[str, str]] = None):
-        """
-        Initializes a ServerTemplate instance.
-
-        Args:
-          client: The API client used to fetch the server template.
-          default_config:  A dictionary of default configuration values.
-        """
-        self._client = client
-        self._condition_evaluator = ConditionEvaluator()
-        self._cache = None
-        self._stringified_default_config = {key: str(value) for key, value in default_config.items()} if default_config else {}
-
-    async def load(self):
-        """Fetches and caches the server template from the Remote Config API."""
-        self._cache = await self._client.get_server_template()
-
-    def set(self, template):
-        """
-        Sets the server template from a string or ServerTemplateData object.
-
-        Args:
-          template: The template to set, either as a JSON string or a ServerTemplateData object.
-        """
-        if isinstance(template, str):
-            try:
-                import json
-                parsed_template = json.loads(template)
-                self._cache = ServerTemplateData(parsed_template)
-            except json.JSONDecodeError as e:
-                raise ValueError(f"Failed to parse the JSON string: {template}. {e}")
-        elif isinstance(template, ServerTemplateData):
-            self._cache = template
-        else:
-            raise TypeError("template must be a string or ServerTemplateData object")
-
-    def evaluate(self, context: Optional[Dict[str, Union[str, int]]] = None) -> 'ServerConfig':
-        """
-        Evaluates the cached server template to produce a ServerConfig.
-
-        Args:
-          context: A dictionary of values to use for evaluating conditions.
-
-        Returns:
-          A ServerConfig object.
-        """
-        if not self._cache:
-            raise ValueError("No Remote Config Server template in cache. Call load() before calling evaluate().")
-
-        context = context or {}
-        evaluated_conditions = self._condition_evaluator.evaluate_conditions(
-            self._cache.conditions, EvaluationContext(**context)
-        )
-
-        config_values = {}
-
-        for key, value in self._stringified_default_config.items():
-            config_values[key] = Value('default', value)
-
-        for key, parameter in self._cache.parameters.items():
-            conditional_values = parameter.get('conditionalValues', {})
-            default_value = parameter.get('defaultValue')
-
-            parameter_value_wrapper = None
-            for condition_name, condition_evaluation in evaluated_conditions.items():
-                if condition_name in conditional_values and condition_evaluation:
-                    parameter_value_wrapper = conditional_values[condition_name]
-                    break
-
-            if parameter_value_wrapper and parameter_value_wrapper.get('useInAppDefault'):
-                logger.info("Using in-app default value for key '%s'", key)
-                continue
-
-            if parameter_value_wrapper:
-                config_values[key] = Value('remote', parameter_value_wrapper.get('value'))
-                continue
-
-            if not default_value:
-                logger.warning("No default value found for key '%s'", key)
-                continue
-
-            if default_value.get('useInAppDefault'):
-                logger.info("Using in-app default value for key '%s'", key)
-                continue
-
-            config_values[key] = Value('remote', default_value.get('value'))
-
-        return ServerConfig(config_values)
-
-
-class ServerConfig:
-    """Represents a Remote Config Server Side Config."""
-
-    def __init__(self, config_values):
-        self._config_values = config_values
-
-    def get_boolean(self, key):
-        return self._config_values[key].as_boolean()
-
-    def get_string(self, key):
-        return self._config_values[key].as_string()
+async def get_server_template(app: App = None, default_config: Optional[Dict[str, str]] = None):
+    """Initializes a new ServerTemplate instance and fetches the server template.
 
-    def get_int(self, key):
-        return int(self._config_values[key].as_number())
+    Args:
+        app: App instance to be used. This is optional and the default app instance will
+            be used if not present.
+        default_config: The default config to be used in the evaluated config.
 
-    def get_value(self, key):
-        return self._config_values[key]
-
-class Value:
+    Returns:
+        ServerTemplate: An object having the cached server template to be used for evaluation.
     """
-    Represents a value fetched from Remote Config.
+    template = init_server_template(app=app, default_config=default_config)
+    await template.load()
+    return template
+
+def init_server_template(app: App = None, default_config: Optional[Dict[str, str]] = None,
+                         template_data: Optional[ServerTemplateData] = None):
+    """Initializes a new ServerTemplate instance.
+
+    Args:
+        app: App instance to be used. This is optional and the default app instance will
+            be used if not present.
+        default_config: The default config to be used in the evaluated config.
+        template_data: An optional template data to be set on initialization.
+
+    Returns:
+        ServerTemplate: A new ServerTemplate instance initialized with an optional
+        template and config.
+    """
+    template = ServerTemplate(app=app, default_config=default_config)
+    if template_data is not None:
+        template.set(template_data)
+    return template
+
+class _Value:
+    """Represents a value fetched from Remote Config.
     """
     DEFAULT_VALUE_FOR_BOOLEAN = False
     DEFAULT_VALUE_FOR_STRING = ''
@@ -563,8 +568,7 @@ class Value:
     BOOLEAN_TRUTHY_VALUES = ['1', 'true', 't', 'yes', 'y', 'on']
 
     def __init__(self, source: ValueSource, value: str = DEFAULT_VALUE_FOR_STRING):
-        """
-        Initializes a Value instance.
+        """Initializes a Value instance.
 
         Args:
           source: The source of the value (e.g., 'default', 'remote', 'static').
@@ -595,3 +599,4 @@ def as_number(self) -> float:
     def get_source(self) -> ValueSource:
         """Returns the source of the value."""
         return self.source
+        
\ No newline at end of file

From b2d8b4f011c2a1a10382d32d1a88a67042b89eb0 Mon Sep 17 00:00:00 2001
From: Varun Rathore <varunrathore@google.com>
Date: Sat, 2 Nov 2024 18:53:10 +0530
Subject: [PATCH 03/15] Add farmhash to extension whitelist pkg

---
 .pylintrc                       | 2 +-
 firebase_admin/_http_client.py  | 8 ++++++++
 firebase_admin/remote_config.py | 3 +--
 3 files changed, 10 insertions(+), 3 deletions(-)

diff --git a/.pylintrc b/.pylintrc
index 2155853c7..cac6d3943 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -32,7 +32,7 @@ unsafe-load-any-extension=no
 # A comma-separated list of package or module names from where C extensions may
 # be loaded. Extensions are loading into the active Python interpreter and may
 # run arbitrary code
-extension-pkg-whitelist=
+extension-pkg-whitelist= farmhash
 
 # Allow optimization of some AST trees. This will activate a peephole AST
 # optimizer, which will apply various small optimizations. For instance, it can
diff --git a/firebase_admin/_http_client.py b/firebase_admin/_http_client.py
index d259faddf..ab6c6b954 100644
--- a/firebase_admin/_http_client.py
+++ b/firebase_admin/_http_client.py
@@ -148,3 +148,11 @@ def __init__(self, **kwargs):
 
     def parse_body(self, resp):
         return resp.json()
+
+class RemoteConfigApiClient(HttpClient):
+    """An HTTP client that parses response messages as JSON."""
+    def __init__(self, **kwargs):
+        HttpClient.__init__(self, **kwargs)
+    def parse_body(self, resp):
+        return resp.json()
+        
\ No newline at end of file
diff --git a/firebase_admin/remote_config.py b/firebase_admin/remote_config.py
index 4411db4de..f3e52c575 100644
--- a/firebase_admin/remote_config.py
+++ b/firebase_admin/remote_config.py
@@ -392,7 +392,7 @@ def hash_seeded_randomization_id(self, seeded_randomization_id: str) -> int:
         Returns:
           The hashed value.
         """
-        hash64 = farmhash.hash64withseed(seeded_randomization_id)
+        hash64 = farmhash.fingerprint64(seeded_randomization_id)
         return abs(hash64)
     def evaluate_custom_signal_condition(self, custom_signal_condition,
                                          context) -> bool:
@@ -599,4 +599,3 @@ def as_number(self) -> float:
     def get_source(self) -> ValueSource:
         """Returns the source of the value."""
         return self.source
-        
\ No newline at end of file

From 76b9b5fdb0d7d52d0bd1d887ddba78e48c27f3ad Mon Sep 17 00:00:00 2001
From: Varun Rathore <varunrathore@google.com>
Date: Sat, 2 Nov 2024 19:22:20 +0530
Subject: [PATCH 04/15] Replace farmhash to hashlib

---
 .pylintrc                       | 2 +-
 firebase_admin/remote_config.py | 6 ++++--
 requirements.txt                | 3 +--
 3 files changed, 6 insertions(+), 5 deletions(-)

diff --git a/.pylintrc b/.pylintrc
index cac6d3943..f9549f93b 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -32,7 +32,7 @@ unsafe-load-any-extension=no
 # A comma-separated list of package or module names from where C extensions may
 # be loaded. Extensions are loading into the active Python interpreter and may
 # run arbitrary code
-extension-pkg-whitelist= farmhash
+extension-pkg-whitelist= 
 
 # Allow optimization of some AST trees. This will activate a peephole AST
 # optimizer, which will apply various small optimizations. For instance, it can
diff --git a/firebase_admin/remote_config.py b/firebase_admin/remote_config.py
index f3e52c575..4b83fef8b 100644
--- a/firebase_admin/remote_config.py
+++ b/firebase_admin/remote_config.py
@@ -21,7 +21,7 @@
 from typing import Dict, Optional, Literal, Callable, Union
 from enum import Enum
 import re
-import farmhash
+import hashlib
 from firebase_admin import App, _http_client, _utils
 import firebase_admin
 
@@ -392,7 +392,9 @@ def hash_seeded_randomization_id(self, seeded_randomization_id: str) -> int:
         Returns:
           The hashed value.
         """
-        hash64 = farmhash.fingerprint64(seeded_randomization_id)
+        hash_object = hashlib.sha256()
+        hash_object.update(seeded_randomization_id)
+        hash64 = hash_object.hexdigest()
         return abs(hash64)
     def evaluate_custom_signal_condition(self, custom_signal_condition,
                                          context) -> bool:
diff --git a/requirements.txt b/requirements.txt
index c726b758a..acf09438b 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -11,5 +11,4 @@ google-api-core[grpc] >= 1.22.1, < 3.0.0dev; platform.python_implementation != '
 google-api-python-client >= 1.7.8
 google-cloud-firestore >= 2.9.1; platform.python_implementation != 'PyPy'
 google-cloud-storage >= 1.37.1
-pyjwt[crypto] >= 2.5.0
-pyfarmhash >= 0.4.0
\ No newline at end of file
+pyjwt[crypto] >= 2.5.0
\ No newline at end of file

From c63da12ef1ab885437b7963b36f9437ba0624b56 Mon Sep 17 00:00:00 2001
From: Varun Rathore <varunrathore@google.com>
Date: Mon, 4 Nov 2024 13:46:27 +0530
Subject: [PATCH 05/15] Added unit testcase

---
 firebase_admin/remote_config.py | 175 ++++----
 tests/test_remote_config.py     | 684 ++++++++++++++++++++++++++++++++
 tests/testutils.py              |  45 +++
 3 files changed, 821 insertions(+), 83 deletions(-)
 create mode 100644 tests/test_remote_config.py

diff --git a/firebase_admin/remote_config.py b/firebase_admin/remote_config.py
index 4b83fef8b..c51e84f88 100644
--- a/firebase_admin/remote_config.py
+++ b/firebase_admin/remote_config.py
@@ -32,6 +32,13 @@
 _REMOTE_CONFIG_ATTRIBUTE = '_remoteconfig'
 MAX_CONDITION_RECURSION_DEPTH = 10
 ValueSource = Literal['default', 'remote', 'static']  # Define the ValueSource type
+class PercentConditionOperator(Enum):
+    """Enum representing the available operators for percent conditions.
+    """
+    LESS_OR_EQUAL = "LESS_OR_EQUAL"
+    GREATER_THAN = "GREATER_THAN"
+    BETWEEN = "BETWEEN"
+    UNKNOWN = "UNKNOWN"
 
 class CustomSignalOperator(Enum):
     """Enum representing the available operators for custom signal conditions.
@@ -52,6 +59,7 @@ class CustomSignalOperator(Enum):
     SEMANTIC_VERSION_NOT_EQUAL = "SEMANTIC_VERSION_NOT_EQUAL"
     SEMANTIC_VERSION_GREATER_THAN = "SEMANTIC_VERSION_GREATER_THAN"
     SEMANTIC_VERSION_GREATER_EQUAL = "SEMANTIC_VERSION_GREATER_EQUAL"
+    UNKNOWN = "UNKNOWN"
 
 class ServerTemplateData:
     """Represents a Server Template Data class."""
@@ -131,13 +139,13 @@ def evaluate(self, context: Optional[Dict[str, Union[str, int]]] = None) -> 'Ser
                             Call load() before calling evaluate().""")
         context = context or {}
         config_values = {}
-
         # Initializes config Value objects with default values.
-        for key, value in self._stringified_default_config.items():
-            config_values[key] = _Value('default', value)
-
-        self._evaluator = _ConditionEvaluator(self._cache.conditions, context,
-                                              config_values, self._cache.parameters)
+        if self._stringified_default_config is not None:
+            for key, value in json.loads(self._stringified_default_config).items():
+                config_values[key] = _Value('default', value)
+        self._evaluator = _ConditionEvaluator(self._cache.conditions,
+                                              self._cache.parameters, context,
+                                              config_values)
         return ServerConfig(config_values=self._evaluator.evaluate())
 
     def set(self, template):
@@ -156,13 +164,13 @@ def __init__(self, config_values):
         self._config_values = config_values # dictionary of param key to values
 
     def get_boolean(self, key):
-        return bool(self.get_value(key))
+        return self.get_value(key).as_boolean()
 
     def get_string(self, key):
-        return str(self.get_value(key))
+        return self.get_value(key).as_string()
 
     def get_int(self, key):
-        return int(self.get_value(key))
+        return self.get_value(key).as_number()
 
     def get_value(self, key):
         return self._config_values[key]
@@ -209,7 +217,7 @@ def _get_url_prefix(self):
 class _ConditionEvaluator:
     """Internal class that facilitates sending requests to the Firebase Remote
     Config backend API."""
-    def __init__(self, context, conditions, config_values, parameters):
+    def __init__(self, conditions, parameters, context, config_values):
         self._context = context
         self._conditions = conditions
         self._parameters = parameters
@@ -221,51 +229,53 @@ def evaluate(self):
         evaluated_conditions = self.evaluate_conditions(self._conditions, self._context)
 
         # Overlays config Value objects derived by evaluating the template.
-        for key, parameter in self._parameters.items():
-            conditional_values = parameter.conditional_values or {}
-            default_value = parameter.default_value or {}
-            parameter_value_wrapper = None
-
-            # Iterates in order over condition list. If there is a value associated
-            # with a condition, this checks if the condition is true.
-            for condition_name, condition_evaluation in evaluated_conditions.items():
-                if condition_name in conditional_values and condition_evaluation:
-                    parameter_value_wrapper = conditional_values[condition_name]
-                    break
-            if parameter_value_wrapper and parameter_value_wrapper.get('useInAppDefault'):
-                logger.info("Using in-app default value for key '%s'", key)
-                continue
-
-            if parameter_value_wrapper:
-                parameter_value = parameter_value_wrapper.value
-                self._config_values[key] = _Value('remote', parameter_value)
-                continue
-
-            if not default_value:
-                logger.warning("No default value found for key '%s'", key)
-                continue
-
-            if default_value.get('useInAppDefault'):
-                logger.info("Using in-app default value for key '%s'", key)
-                continue
-
-            self._config_values[key] = _Value('remote', default_value.get('value'))
+       # evaluated_conditions = None
+        if self._parameters is not None:
+            for key, parameter in self._parameters.items():
+                conditional_values = parameter.get('conditionalValues', {})
+                default_value = parameter.get('defaultValue', {})
+                parameter_value_wrapper = None
+                # Iterates in order over condition list. If there is a value associated
+                # with a condition, this checks if the condition is true.
+                if evaluated_conditions is not None:
+                    for condition_name, condition_evaluation in evaluated_conditions.items():
+                        if condition_name in conditional_values and condition_evaluation:
+                            parameter_value_wrapper = conditional_values[condition_name]
+                            break
+
+                if parameter_value_wrapper and parameter_value_wrapper.get('useInAppDefault'):
+                    logger.info("Using in-app default value for key '%s'", key)
+                    continue
+
+                if parameter_value_wrapper:
+                    parameter_value = parameter_value_wrapper.get('value')
+                    self._config_values[key] = _Value('remote', parameter_value)
+                    continue
+
+                if not default_value:
+                    logger.warning("No default value found for key '%s'", key)
+                    continue
+
+                if default_value.get('useInAppDefault'):
+                    logger.info("Using in-app default value for key '%s'", key)
+                    continue
+                self._config_values[key] = _Value('remote', default_value.get('value'))
         return self._config_values
 
-    def evaluate_conditions(self, named_conditions, context)-> Dict[str, bool]:
-        """Evaluates a list of named conditions and returns a dictionary of results.
+    def evaluate_conditions(self, conditions, context)-> Dict[str, bool]:
+        """Evaluates a list of conditions and returns a dictionary of results.
 
         Args:
-          named_conditions: A list of NamedCondition objects.
+          conditions: A list of NamedCondition objects.
           context: An EvaluationContext object.
 
         Returns:
           A dictionary mapping condition names to boolean evaluation results.
         """
         evaluated_conditions = {}
-        for named_condition in named_conditions:
-            evaluated_conditions[named_condition.name] = self.evaluate_condition(
-                named_condition.condition, context
+        for condition in conditions:
+            evaluated_conditions[condition.get('name')] = self.evaluate_condition(
+                condition.get('condition'), context
             )
         return evaluated_conditions
 
@@ -284,18 +294,20 @@ def evaluate_condition(self, condition, context,
         if nesting_level >= MAX_CONDITION_RECURSION_DEPTH:
             logger.warning("Maximum condition recursion depth exceeded.")
             return False
-        if condition.or_condition:
-            return self.evaluate_or_condition(condition.or_condition, context, nesting_level + 1)
-        if condition.and_condition:
-            return self.evaluate_and_condition(condition.and_condition, context, nesting_level + 1)
-        if condition.true_condition:
+        if condition.get('orCondition') is not None:
+            return self.evaluate_or_condition(condition.get('orCondition'),
+                                              context, nesting_level + 1)
+        if condition.get('andCondition') is not None:
+            return self.evaluate_and_condition(condition.get('andCondition'),
+                                               context, nesting_level + 1)
+        if condition.get('true') is not None:
             return True
-        if condition.false_condition:
+        if condition.get('false') is not None:
             return False
-        if condition.percent_condition:
-            return self.evaluate_percent_condition(condition.percent_condition, context)
-        if condition.custom_signal_condition:
-            return self.evaluate_custom_signal_condition(condition.custom_signal_condition, context)
+        if condition.get('percent') is not None:
+            return self.evaluate_percent_condition(condition.get('percent'), context)
+        if condition.get('customSignal') is not None:
+            return self.evaluate_custom_signal_condition(condition.get('customSignal'), context)
         logger.warning("Unknown condition type encountered.")
         return False
 
@@ -312,7 +324,7 @@ def evaluate_or_condition(self, or_condition,
         Returns:
           True if any of the subconditions are true, False otherwise.
         """
-        sub_conditions = or_condition.conditions or []
+        sub_conditions = or_condition.get('conditions') or []
         for sub_condition in sub_conditions:
             result = self.evaluate_condition(sub_condition, context, nesting_level + 1)
             if result:
@@ -332,7 +344,7 @@ def evaluate_and_condition(self, and_condition,
         Returns:
           True if all of the subconditions are true, False otherwise.
         """
-        sub_conditions = and_condition.conditions or []
+        sub_conditions = and_condition.get('conditions') or []
         for sub_condition in sub_conditions:
             result = self.evaluate_condition(sub_condition, context, nesting_level + 1)
             if not result:
@@ -350,36 +362,33 @@ def evaluate_percent_condition(self, percent_condition,
         Returns:
           True if the condition is met, False otherwise.
         """
-        if not context.randomization_id:
+        if not context.get('randomization_id'):
             logger.warning("Missing randomization ID for percent condition.")
             return False
 
-        seed = percent_condition.seed
-        percent_operator = percent_condition.percent_operator
-        micro_percent = percent_condition.micro_percent or 0
-        micro_percent_range = percent_condition.micro_percent_range
-
+        seed = percent_condition.get('seed')
+        percent_operator = percent_condition.get('percentOperator')
+        micro_percent = percent_condition.get('microPercent')
+        micro_percent_range = percent_condition.get('microPercentRange')
         if not percent_operator:
             logger.warning("Missing percent operator for percent condition.")
             return False
         if micro_percent_range:
-            norm_percent_upper_bound = micro_percent_range.micro_percent_upper_bound
-            norm_percent_lower_bound = micro_percent_range.micro_percent_lower_bound
+            norm_percent_upper_bound = micro_percent_range.get('microPercentUpperBound')
+            norm_percent_lower_bound = micro_percent_range.get('microPercentLowerBound')
         else:
             norm_percent_upper_bound = 0
             norm_percent_lower_bound = 0
         seed_prefix = f"{seed}." if seed else ""
-        string_to_hash = f"{seed_prefix}{context.randomization_id}"
+        string_to_hash = f"{seed_prefix}{context.get('randomization_id')}"
 
         hash64 = self.hash_seeded_randomization_id(string_to_hash)
-
-        instance_micro_percentile = hash64 % (100 * 1_000_000)
-
-        if percent_operator == "LESS_OR_EQUAL":
+        instance_micro_percentile = hash64 % (100 * 1000000)
+        if percent_operator == PercentConditionOperator.LESS_OR_EQUAL:
             return instance_micro_percentile <= micro_percent
-        if percent_operator == "GREATER_THAN":
+        if percent_operator == PercentConditionOperator.GREATER_THAN:
             return instance_micro_percentile > micro_percent
-        if percent_operator == "BETWEEN":
+        if percent_operator == PercentConditionOperator.BETWEEN:
             return norm_percent_lower_bound < instance_micro_percentile <= norm_percent_upper_bound
         logger.warning("Unknown percent operator: %s", percent_operator)
         return False
@@ -393,9 +402,9 @@ def hash_seeded_randomization_id(self, seeded_randomization_id: str) -> int:
           The hashed value.
         """
         hash_object = hashlib.sha256()
-        hash_object.update(seeded_randomization_id)
+        hash_object.update(seeded_randomization_id.encode('utf-8'))
         hash64 = hash_object.hexdigest()
-        return abs(hash64)
+        return abs(int(hash64, 16))
     def evaluate_custom_signal_condition(self, custom_signal_condition,
                                          context) -> bool:
         """Evaluates a custom signal condition.
@@ -407,15 +416,15 @@ def evaluate_custom_signal_condition(self, custom_signal_condition,
         Returns:
           True if the condition is met, False otherwise.
         """
-        custom_signal_operator = custom_signal_condition.custom_signal_operator
-        custom_signal_key = custom_signal_condition.custom_signal_key
-        target_custom_signal_values = custom_signal_condition.target_custom_signal_values
+        custom_signal_operator = custom_signal_condition.get('custom_signal_operator') or {}
+        custom_signal_key = custom_signal_condition.get('custom_signal_key') or {}
+        tgt_custom_signal_values = custom_signal_condition.get('target_custom_signal_values') or {}
 
-        if not all([custom_signal_operator, custom_signal_key, target_custom_signal_values]):
+        if not all([custom_signal_operator, custom_signal_key, tgt_custom_signal_values]):
             logger.warning("Missing operator, key, or target values for custom signal condition.")
             return False
 
-        if not target_custom_signal_values:
+        if not tgt_custom_signal_values:
             return False
         actual_custom_signal_value = getattr(context, custom_signal_key, None)
         if actual_custom_signal_value is None:
@@ -466,14 +475,14 @@ def compare_strings(predicate_fn: Callable[[str, str], bool]) -> bool:
                 bool: True if the predicate function returns True for any target value in the list,
                     False otherwise.
             """
-            for target in target_custom_signal_values:
+            for target in tgt_custom_signal_values:
                 if predicate_fn(target, str(actual_custom_signal_value)):
                     return True
             return False
 
         def compare_numbers(predicate_fn: Callable[[int], bool]) -> bool:
             try:
-                target = float(target_custom_signal_values[0])
+                target = float(tgt_custom_signal_values[0])
                 actual = float(actual_custom_signal_value)
                 result = -1 if actual < target else 1 if actual > target else 0
                 return predicate_fn(result)
@@ -494,7 +503,7 @@ def compare_semantic_versions(predicate_fn: Callable[[int], bool]) -> bool:
             False otherwise.
             """
             return compare_versions(str(actual_custom_signal_value),
-                                    str(target_custom_signal_values[0]), predicate_fn)
+                                    str(tgt_custom_signal_values[0]), predicate_fn)
         def compare_versions(version1: str, version2: str,
                              predicate_fn: Callable[[int], bool]) -> bool:
             """Compares two semantic version strings.
@@ -587,7 +596,7 @@ def as_boolean(self) -> bool:
         """Returns the value as a boolean."""
         if self.source == 'static':
             return self.DEFAULT_VALUE_FOR_BOOLEAN
-        return self.value.lower() in self.BOOLEAN_TRUTHY_VALUES
+        return str(self.value).lower() in self.BOOLEAN_TRUTHY_VALUES
 
     def as_number(self) -> float:
         """Returns the value as a number."""
diff --git a/tests/test_remote_config.py b/tests/test_remote_config.py
new file mode 100644
index 000000000..fa366d740
--- /dev/null
+++ b/tests/test_remote_config.py
@@ -0,0 +1,684 @@
+# Copyright 2017 Google Inc.
+#
+# 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.
+
+"""Tests for firebase_admin.remote_config."""
+import json
+import uuid
+import firebase_admin
+from firebase_admin.remote_config import (
+    _REMOTE_CONFIG_ATTRIBUTE,
+    _RemoteConfigService,
+    PercentConditionOperator)
+from firebase_admin import remote_config, _utils
+from tests import testutils
+
+
+VERSION_INFO = {
+    'versionNumber': '86',
+    'updateOrigin': 'ADMIN_SDK_NODE',
+    'updateType': 'INCREMENTAL_UPDATE',
+    'updateUser': {
+        'email': 'firebase-adminsdk@gserviceaccount.com'
+    },
+    'description': 'production version',
+    'updateTime': '2020-06-15T16:45:03.541527Z'
+    }
+SERVER_REMOTE_CONFIG_RESPONSE = {
+    'conditions': [
+        {
+            'name': 'ios',
+            'condition': {
+                'orCondition': {
+                    'conditions': [
+                        {
+                            'andCondition': {
+                                'conditions': [
+                                    {'true': {}}
+                                ]
+                            }
+                        }
+                    ]
+                }
+            }
+        },
+    ],
+    'parameters': {
+        'holiday_promo_enabled': {
+            'defaultValue': {'value': 'true'},
+            'conditionalValues': {'ios': {'useInAppDefault': 'true'}}
+        },
+    },
+    'parameterGroups': '',
+    'etag': 'etag-123456789012-5',
+    'version': VERSION_INFO,
+    }
+
+class MockAdapter(testutils.MockAdapter):
+    """A Mock HTTP Adapter that Firebase Remote Config with ETag in header."""
+
+    ETAG = '0'
+
+    def __init__(self, data, status, recorder, etag=ETAG):
+        testutils.MockAdapter.__init__(self, data, status, recorder)
+        self._etag = etag
+
+    def send(self, request, **kwargs):
+        resp = super(MockAdapter, self).send(request, **kwargs)
+        resp.headers = {'ETag': self._etag}
+        return resp
+
+
+class TestGetServerTemplate:
+    _DEFAULT_APP = firebase_admin.initialize_app(testutils.MockCredential(), name='no_project_id')
+    _RC_INSTANCE = _utils.get_app_service(_DEFAULT_APP,
+                                          _REMOTE_CONFIG_ATTRIBUTE, _RemoteConfigService)
+    _DEFAULT_RESPONSE = json.dumps({
+        'parameters': {
+            'test_key': 'test_value'
+        },
+        'conditions': {},
+        'parameterGroups': {},
+        'version': 'test'
+        })
+
+    def test_rc_instance_get_server_template(self):
+        recorder = []
+        self._RC_INSTANCE._client.session.mount(
+            'https://firebaseremoteconfig.googleapis.com',
+            MockAdapter(self._DEFAULT_RESPONSE, 200, recorder))
+
+        template = self._RC_INSTANCE.get_server_template()
+
+        assert template.parameters == dict(test_key="test_value")
+        assert str(template.version) == 'test'
+
+    def test_rc_instance_return_conditional_values(self):
+        recorder = []
+        self._RC_INSTANCE._client.session.mount(
+            'https://firebaseremoteconfig.googleapis.com',
+            MockAdapter(self._DEFAULT_RESPONSE, 200, recorder))
+        default_config = {'param1': 'in_app_default_param1', 'param3': 'in_app_default_param3'}
+        condition = {
+            'name': 'is_true',
+            'condition': {
+                'orCondition': {
+                    'conditions': [
+                        {
+                            'andCondition': {
+                                'conditions': [
+                                    {
+                                        'name': '',
+                                        'true': {
+                                        }
+                                    }
+                                ]
+                            }
+                        }
+                    ]
+                }
+            }
+        }
+        template_data = {
+            'conditions': [condition],
+            'parameters': {
+                'is_enabled': {
+                    'defaultValue': {'value': 'false'},
+                    'conditionalValues': {'is_true': {'value': 'true'}}
+                },
+            },
+            'parameterGroups': '',
+            'version': '',
+            'etag': '123'
+        }
+        server_template = remote_config.ServerTemplate(self._DEFAULT_APP, default_config)
+        server_template.set(remote_config.ServerTemplateData('etag', template_data))
+        server_config = server_template.evaluate()
+        assert server_config.get_boolean('is_enabled')
+
+    def test_rc_instance_return_conditional_values_true(self):
+        recorder = []
+        self._RC_INSTANCE._client.session.mount(
+            'https://firebaseremoteconfig.googleapis.com',
+            MockAdapter(self._DEFAULT_RESPONSE, 200, recorder))
+        default_config = {'param1': 'in_app_default_param1', 'param3': 'in_app_default_param3'}
+        condition = {
+            'name': 'is_true',
+            'condition': {
+                'orCondition': {
+                    'conditions': [
+                        {
+                            'andCondition': {
+                                'conditions': [
+                                    {
+                                        'name': '',
+                                        'true': {
+                                        }
+                                    }
+                                ]
+                            }
+                        }
+                    ]
+                }
+            }
+        }
+        template_data = {
+            'conditions': [condition],
+            'parameters': {
+                'is_enabled': {
+                    'defaultValue': {'value': 'false'},
+                    'conditionalValues': {'is_true': {'value': 'true'}}
+                },
+            },
+            'parameterGroups': '',
+            'version': '',
+            'etag': '123'
+        }
+        server_template = remote_config.ServerTemplate(self._DEFAULT_APP, default_config)
+        server_template.set(remote_config.ServerTemplateData('etag', template_data))
+        server_config = server_template.evaluate()
+        assert server_config.get_boolean('is_enabled')
+
+    def test_rc_instance_return_conditional_values_honor_order(self):
+        recorder = []
+        self._RC_INSTANCE._client.session.mount(
+            'https://firebaseremoteconfig.googleapis.com',
+            MockAdapter(self._DEFAULT_RESPONSE, 200, recorder))
+        default_config = {'param1': 'in_app_default_param1', 'param3': 'in_app_default_param3'}
+        template_data = {
+            'conditions': [
+                {
+                    'name': 'is_true',
+                    'condition': {
+                        'orCondition': {
+                            'conditions': [
+                                {
+                                    'andCondition': {
+                                        'conditions': [
+                                            {
+                                                'true': {
+                                                }
+                                            }
+                                        ]
+                                    }
+                                }
+                            ]
+                        }
+                    }
+                },
+                {
+                    'name': 'is_true_too',
+                    'condition': {
+                        'orCondition': {
+                            'conditions': [
+                                {
+                                    'andCondition': {
+                                        'conditions': [
+                                            {
+                                                'true': {
+                                                }
+                                            }
+                                        ]
+                                    }
+                                }
+                            ]
+                        }
+                    }
+                }
+            ],
+            'parameters': {
+                'dog_type': {
+                    'defaultValue': {'value': 'chihuahua'},
+                    'conditionalValues': {
+                        'is_true_too': {'value': 'dachshund'},
+                        'is_true': {'value': 'corgi'}
+                    }
+                },
+            },
+            'parameterGroups':'',
+            'version':'',
+            'etag': '123'
+        }
+        server_template = remote_config.ServerTemplate(self._DEFAULT_APP, default_config)
+        server_template.set(remote_config.ServerTemplateData('etag', template_data))
+        server_config = server_template.evaluate()
+        assert server_config.get_string('dog_type') == 'corgi'
+
+    def test_rc_instance_return_conditional_values_honor_order_final(self):
+        recorder = []
+        self._RC_INSTANCE._client.session.mount(
+            'https://firebaseremoteconfig.googleapis.com',
+            MockAdapter(self._DEFAULT_RESPONSE, 200, recorder))
+        default_config = {'param1': 'in_app_default_param1', 'param3': 'in_app_default_param3'}
+        template_data = {
+            'conditions': [
+                {
+                    'name': 'is_true',
+                    'condition': {
+                        'orCondition': {
+                            'conditions': [
+                                {
+                                    'andCondition': {
+                                        'conditions': [
+                                            {
+                                                'true': {
+                                                }
+                                            }
+                                        ]
+                                    }
+                                }
+                            ]
+                        }
+                    }
+                },
+                {
+                    'name': 'is_true_too',
+                    'condition': {
+                        'orCondition': {
+                            'conditions': [
+                                {
+                                    'andCondition': {
+                                        'conditions': [
+                                            {
+                                                'true': {
+                                                }
+                                            }
+                                        ]
+                                    }
+                                }
+                            ]
+                        }
+                    }
+                }
+            ],
+            'parameters': {
+                'dog_type': {
+                    'defaultValue': {'value': 'chihuahua'},
+                    'conditionalValues': {
+                        'is_true_too': {'value': 'dachshund'},
+                        'is_true': {'value': 'corgi'}
+                    }
+                },
+            },
+            'parameterGroups':'',
+            'version':'',
+            'etag': '123'
+        }
+        server_template = remote_config.ServerTemplate(self._DEFAULT_APP, default_config)
+        server_template.set(remote_config.ServerTemplateData('etag', template_data))
+        server_config = server_template.evaluate()
+        assert server_config.get_string('dog_type') == 'corgi'
+
+    def test_rc_instance_evaluate_default_when_no_param(self):
+        recorder = []
+        self._RC_INSTANCE._client.session.mount(
+            'https://firebaseremoteconfig.googleapis.com',
+            MockAdapter(self._DEFAULT_RESPONSE, 200, recorder))
+        default_config = {'promo_enabled': False, 'promo_discount': 20,}
+        template_data = SERVER_REMOTE_CONFIG_RESPONSE
+        template_data['parameters'] = {}
+        server_template = remote_config.ServerTemplate(self._DEFAULT_APP, default_config)
+        server_template.set(remote_config.ServerTemplateData('etag', template_data))
+        server_config = server_template.evaluate()
+        assert server_config.get_boolean('promo_enabled') == default_config.get('promo_enabled')
+        assert server_config.get_int('promo_discount') == default_config.get('promo_discount')
+
+    def test_rc_instance_evaluate_default_when_no_default_value(self):
+        recorder = []
+        self._RC_INSTANCE._client.session.mount(
+            'https://firebaseremoteconfig.googleapis.com',
+            MockAdapter(self._DEFAULT_RESPONSE, 200, recorder))
+        default_config = {'default_value': 'local default'}
+        template_data = SERVER_REMOTE_CONFIG_RESPONSE
+        template_data['parameters'] = {
+            'default_value': {}
+        }
+        server_template = remote_config.ServerTemplate(self._DEFAULT_APP, default_config)
+        server_template.set(remote_config.ServerTemplateData('etag', template_data))
+        server_config = server_template.evaluate()
+        assert server_config.get_string('default_value') == default_config.get('default_value')
+
+    def test_rc_instance_evaluate_default_when_in_default(self):
+        template_data = SERVER_REMOTE_CONFIG_RESPONSE
+        template_data['parameters'] = {
+            'remote_default_value': {}
+        }
+        default_config = {
+            'inapp_default': '🐕'
+        }
+        server_template = remote_config.ServerTemplate(self._DEFAULT_APP, default_config)
+        server_template.set(remote_config.ServerTemplateData('etag', template_data))
+        server_config = server_template.evaluate()
+        assert server_config.get_string('inapp_default') == default_config.get('inapp_default')
+
+    def test_rc_instance_evaluate_default_when_defined(self):
+        template_data = SERVER_REMOTE_CONFIG_RESPONSE
+        template_data['parameters'] = {}
+        default_config = {
+            'dog_type': 'shiba'
+        }
+        server_template = remote_config.ServerTemplate(self._DEFAULT_APP, default_config)
+        server_template.set(remote_config.ServerTemplateData('etag', template_data))
+        server_config = server_template.evaluate()
+        assert server_config.get_value('dog_type').as_string() == 'shiba'
+        assert server_config.get_value('dog_type').get_source() == 'default'
+
+    def test_rc_instance_evaluate_return_numeric_value(self):
+        template_data = SERVER_REMOTE_CONFIG_RESPONSE
+        default_config = {
+            'dog_age': 12
+        }
+        server_template = remote_config.ServerTemplate(self._DEFAULT_APP, default_config)
+        server_template.set(remote_config.ServerTemplateData('etag', template_data))
+        server_config = server_template.evaluate()
+        assert server_config.get_int('dog_age') == 12
+
+    def test_rc_instance_evaluate_return__value(self):
+        template_data = SERVER_REMOTE_CONFIG_RESPONSE
+        default_config = {
+            'dog_is_cute': True
+        }
+        server_template = remote_config.ServerTemplate(self._DEFAULT_APP, default_config)
+        server_template.set(remote_config.ServerTemplateData('etag', template_data))
+        server_config = server_template.evaluate()
+        assert server_config.get_int('dog_is_cute')
+
+    def test_rc_instance_evaluate_unknown_operator_false(self):
+        condition = {
+            'name': 'is_true',
+            'condition': {
+                'orCondition': {
+                    'conditions': [{
+                        'andCondition': {
+                            'conditions': [{
+                                'percent': {
+                                    'percentOperator': PercentConditionOperator.UNKNOWN
+                                }
+                            }],
+                        }
+                    }]
+                }
+            }
+        }
+        default_config = {
+            'dog_is_cute': True
+        }
+        template_data = {
+            'conditions': [condition],
+            'parameters': {
+                'is_enabled': {
+                    'defaultValue': {'value': 'false'},
+                    'conditionalValues': {'is_true': {'value': 'true'}}
+                },
+            },
+            'parameterGroups':'',
+            'version':'',
+            'etag': '123'
+        }
+        context = {'randomization_id': '123'}
+        server_template = remote_config.ServerTemplate(self._DEFAULT_APP, default_config)
+        server_template.set(remote_config.ServerTemplateData('etag', template_data))
+        server_config = server_template.evaluate(context)
+        assert not server_config.get_boolean('is_enabled')
+
+    def test_rc_instance_evaluate_less_max_equal_true(self):
+        condition = {
+            'name': 'is_true',
+            'condition': {
+                'orCondition': {
+                    'conditions': [{
+                        'andCondition': {
+                            'conditions': [{
+                                'percent': {
+                                    'percentOperator': PercentConditionOperator.LESS_OR_EQUAL,
+                                    'seed': 'abcdef',
+                                    'microPercent': 100_000_000
+                                }
+                            }],
+                        }
+                    }]
+                }
+            }
+        }
+        default_config = {
+            'dog_is_cute': True
+        }
+        template_data = {
+            'conditions': [condition],
+            'parameters': {
+                'is_enabled': {
+                    'defaultValue': {'value': 'false'},
+                    'conditionalValues': {'is_true': {'value': 'true'}}
+                },
+            },
+            'parameterGroups':'',
+            'version':'',
+            'etag': '123'
+        }
+        context = {'randomization_id': '123'}
+        server_template = remote_config.ServerTemplate(self._DEFAULT_APP, default_config)
+        server_template.set(remote_config.ServerTemplateData('etag', template_data))
+        server_config = server_template.evaluate(context)
+        assert server_config.get_boolean('is_enabled')
+
+    def test_rc_instance_evaluate_min_max_equal_true(self):
+        condition = {
+            'name': 'is_true',
+            'condition': {
+                'orCondition': {
+                    'conditions': [{
+                        'andCondition': {
+                            'conditions': [{
+                                'percent': {
+                                    'percentOperator': PercentConditionOperator.BETWEEN,
+                                    'seed': 'abcdef',
+                                    'microPercentRange': {
+                                        'microPercentLowerBound': 0,
+                                        'microPercentUpperBound': 100_000_000
+                                    }
+                                }
+                            }],
+                        }
+                    }]
+                }
+            }
+        }
+        default_config = {
+            'dog_is_cute': True
+        }
+        template_data = {
+            'conditions': [condition],
+            'parameters': {
+                'is_enabled': {
+                    'defaultValue': {'value': 'false'},
+                    'conditionalValues': {'is_true': {'value': 'true'}}
+                },
+            },
+            'parameterGroups':'',
+            'version':'',
+            'etag': '123'
+        }
+        context = {'randomization_id': '123'}
+        server_template = remote_config.ServerTemplate(self._DEFAULT_APP, default_config)
+        server_template.set(remote_config.ServerTemplateData('etag', template_data))
+        server_config = server_template.evaluate(context)
+        assert server_config.get_boolean('is_enabled')
+
+    def test_rc_instance_evaluate_min_max_equal_false(self):
+        condition = {
+            'name': 'is_true',
+            'condition': {
+                'orCondition': {
+                    'conditions': [{
+                        'andCondition': {
+                            'conditions': [{
+                                'percent': {
+                                    'percentOperator': PercentConditionOperator.BETWEEN,
+                                    'seed': 'abcdef',
+                                    'microPercentRange': {
+                                        'microPercentLowerBound': 50000000,
+                                        'microPercentUpperBound': 50000000
+                                    }
+                                }
+                            }],
+                        }
+                    }]
+                }
+            }
+        }
+        default_config = {
+            'dog_is_cute': True
+        }
+        template_data = {
+            'conditions': [condition],
+            'parameters': {
+                'is_enabled': {
+                    'defaultValue': {'value': 'false'},
+                    'conditionalValues': {'is_true': {'value': 'true'}}
+                },
+            },
+            'parameterGroups':'',
+            'version':'',
+            'etag': '123'
+        }
+        context = {'randomization_id': '123'}
+        server_template = remote_config.ServerTemplate(self._DEFAULT_APP, default_config)
+        server_template.set(remote_config.ServerTemplateData('etag', template_data))
+        server_config = server_template.evaluate(context)
+        assert not server_config.get_boolean('is_enabled')
+
+    def test_rc_instance_evaluate_less_or_equal_approx(self):
+        condition = {
+            'name': 'is_true',
+            'condition': {
+                'orCondition': {
+                    'conditions': [{
+                        'andCondition': {
+                            'conditions': [{
+                                'percent': {
+                                    'percentOperator': PercentConditionOperator.LESS_OR_EQUAL,
+                                    'seed': 'abcdef',
+                                    'microPercent': 10_000_000 # 10%
+                                }
+                            }],
+                        }
+                    }]
+                }
+            }
+        }
+        default_config = {
+            'dog_is_cute': True
+        }
+
+        server_template = remote_config.ServerTemplate(self._DEFAULT_APP, default_config)
+        truthy_assignments = self.evaluate_random_assignments(condition, 100000, server_template)
+        tolerance = 284
+        assert truthy_assignments >= 10000 - tolerance
+        assert truthy_assignments <= 10000 + tolerance
+
+    def test_rc_instance_evaluate_between_approx(self):
+        condition = {
+            'name': 'is_true',
+            'condition': {
+                'orCondition': {
+                    'conditions': [{
+                        'andCondition': {
+                            'conditions': [{
+                                'percent': {
+                                    'percentOperator': PercentConditionOperator.BETWEEN,
+                                    'seed': 'abcdef',
+                                    'microPercentRange': {
+                                        'microPercentLowerBound': 40_000_000,
+                                        'microPercentUpperBound': 60_000_000
+                                    }
+                                }
+                            }],
+                        }
+                    }]
+                }
+            }
+        }
+        default_config = {
+            'dog_is_cute': True
+        }
+
+        server_template = remote_config.ServerTemplate(self._DEFAULT_APP, default_config)
+        truthy_assignments = self.evaluate_random_assignments(condition, 100000, server_template)
+        tolerance = 379
+        assert truthy_assignments >= 20000 - tolerance
+        assert truthy_assignments <= 20000 + tolerance
+
+    def test_rc_instance_evaluate_between_interquartile_range_approx(self):
+        condition = {
+            'name': 'is_true',
+            'condition': {
+                'orCondition': {
+                    'conditions': [{
+                        'andCondition': {
+                            'conditions': [{
+                                'percent': {
+                                    'percentOperator': PercentConditionOperator.BETWEEN,
+                                    'seed': 'abcdef',
+                                    'microPercentRange': {
+                                        'microPercentLowerBound': 25_000_000,
+                                        'microPercentUpperBound': 75_000_000
+                                    }
+                                }
+                            }],
+                        }
+                    }]
+                }
+            }
+        }
+        default_config = {
+            'dog_is_cute': True
+        }
+
+        server_template = remote_config.ServerTemplate(self._DEFAULT_APP, default_config)
+        truthy_assignments = self.evaluate_random_assignments(condition, 100000, server_template)
+        tolerance = 474
+        assert truthy_assignments >= 50000 - tolerance
+        assert truthy_assignments <= 50000 + tolerance
+
+    def evaluate_random_assignments(self, condition, num_of_assignments, server_template) -> int:
+        """Evaluates random assignments based on a condition.
+
+        Args:
+        condition: The condition to evaluate.
+        num_of_assignments: The number of assignments to generate.
+        condition_evaluator: An instance of the ConditionEvaluator class.
+
+        Returns:
+            int: The number of assignments that evaluated to true.
+        """
+        eval_true_count = 0
+        template_data = {
+            'conditions': [condition],
+            'parameters': {
+                'is_enabled': {
+                    'defaultValue': {'value': 'false'},
+                    'conditionalValues': {'is_true': {'value': 'true'}}
+                },
+            },
+            'parameterGroups':'',
+            'version':'',
+            'etag': '123'
+        }
+        server_template.set(remote_config.ServerTemplateData('etag', template_data))
+        for _ in range(num_of_assignments):
+            context = {'randomization_id': str(uuid.uuid4())}
+            result = server_template.evaluate(context)
+            if result.get_boolean('is_enabled') is True:
+                eval_true_count += 1
+
+        return eval_true_count
diff --git a/tests/testutils.py b/tests/testutils.py
index ab4fb40cb..12c413989 100644
--- a/tests/testutils.py
+++ b/tests/testutils.py
@@ -218,3 +218,48 @@ def send(self, request, **kwargs): # pylint: disable=arguments-differ
                 resp.raw = io.BytesIO(response.encode())
                 break
         return resp
+
+def build_mock_condition(name, condition):
+    return {
+        'name': name,
+        'condition': condition,
+        # ... other relevant fields ...
+    }
+
+def build_mock_parameter(name, description, value=None,
+                         conditional_values=None, default_value=None):
+    return {
+        'name': name,
+        'description': description,
+        'value': value,
+        'conditionalValues': conditional_values,
+        'defaultValue': default_value,
+        # ... other relevant fields ...
+    }
+
+def build_mock_conditional_value(condition_name, value):
+    return {
+        'conditionName': condition_name,
+        'value': value,
+        # ... other relevant fields ...
+    }
+
+def build_mock_default_value(value):
+    return {
+        'value': value,
+        # ... other relevant fields ...
+    }
+
+def build_mock_parameter_group(name, description, parameters):
+    return {
+        'name': name,
+        'description': description,
+        'parameters': parameters,
+        # ... other relevant fields ...
+    }
+
+def build_mock_version(version_number):
+    return {
+        'versionNumber': version_number,
+        # ... other relevant fields ...
+    }

From 758d6e40f7f7754074c83aaf6d8da81783ace66d Mon Sep 17 00:00:00 2001
From: Varun Rathore <varunrathore@google.com>
Date: Mon, 4 Nov 2024 13:53:12 +0530
Subject: [PATCH 06/15] removed lint error

---
 .pylintrc | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.pylintrc b/.pylintrc
index f9549f93b..2155853c7 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -32,7 +32,7 @@ unsafe-load-any-extension=no
 # A comma-separated list of package or module names from where C extensions may
 # be loaded. Extensions are loading into the active Python interpreter and may
 # run arbitrary code
-extension-pkg-whitelist= 
+extension-pkg-whitelist=
 
 # Allow optimization of some AST trees. This will activate a peephole AST
 # optimizer, which will apply various small optimizations. For instance, it can

From 10dc901783c9b7eb46664af5f102f01c92d4484e Mon Sep 17 00:00:00 2001
From: Varun Rathore <varunrathore@google.com>
Date: Mon, 4 Nov 2024 19:14:08 +0530
Subject: [PATCH 07/15] add mock test

---
 firebase_admin/remote_config.py |   1 -
 tests/test_remote_config.py     | 207 +++++++++++++++++++++-----------
 2 files changed, 138 insertions(+), 70 deletions(-)

diff --git a/firebase_admin/remote_config.py b/firebase_admin/remote_config.py
index c51e84f88..25e8125cb 100644
--- a/firebase_admin/remote_config.py
+++ b/firebase_admin/remote_config.py
@@ -133,7 +133,6 @@ def evaluate(self, context: Optional[Dict[str, Union[str, int]]] = None) -> 'Ser
             ValueError: If the input arguments are invalid.
         """
         # Logic to process the cached template into a ServerConfig here.
-        # TODO: Add Condition evaluator.
         if not self._cache:
             raise ValueError("""No Remote Config Server template in cache.
                             Call load() before calling evaluate().""")
diff --git a/tests/test_remote_config.py b/tests/test_remote_config.py
index fa366d740..a60d4998e 100644
--- a/tests/test_remote_config.py
+++ b/tests/test_remote_config.py
@@ -15,15 +15,19 @@
 """Tests for firebase_admin.remote_config."""
 import json
 import uuid
+from unittest import mock
 import firebase_admin
 from firebase_admin.remote_config import (
     _REMOTE_CONFIG_ATTRIBUTE,
     _RemoteConfigService,
-    PercentConditionOperator)
+    PercentConditionOperator,
+    ServerTemplateData)
 from firebase_admin import remote_config, _utils
 from tests import testutils
 
 
+
+
 VERSION_INFO = {
     'versionNumber': '86',
     'updateOrigin': 'ADMIN_SDK_NODE',
@@ -92,22 +96,25 @@ class TestGetServerTemplate:
         'version': 'test'
         })
 
-    def test_rc_instance_get_server_template(self):
-        recorder = []
-        self._RC_INSTANCE._client.session.mount(
-            'https://firebaseremoteconfig.googleapis.com',
-            MockAdapter(self._DEFAULT_RESPONSE, 200, recorder))
+    def set_up(self):
+        # Create a more specific mock for firebase_admin.App
+        self.mock_app = mock.create_autospec(firebase_admin.App)
+        self.mock_app.project_id = 'mock-project-id'
+        self.mock_app.name = 'mock-app-name'
+
+        # Mock initialize_app to return the mock App instance
+        self.mock_initialize_app = mock.patch('firebase_admin.initialize_app').start()
+        self.mock_initialize_app.return_value = self.mock_app
 
-        template = self._RC_INSTANCE.get_server_template()
+        # Mock the app registry
+        self.mock_get_app = mock.patch('firebase_admin._utils.get_app_service').start()
+        self.mock_get_app.return_value = self.mock_app
 
-        assert template.parameters == dict(test_key="test_value")
-        assert str(template.version) == 'test'
+    def tear_down(self):
+        mock.patch.stopall()
 
     def test_rc_instance_return_conditional_values(self):
-        recorder = []
-        self._RC_INSTANCE._client.session.mount(
-            'https://firebaseremoteconfig.googleapis.com',
-            MockAdapter(self._DEFAULT_RESPONSE, 200, recorder))
+        self.set_up()
         default_config = {'param1': 'in_app_default_param1', 'param3': 'in_app_default_param3'}
         condition = {
             'name': 'is_true',
@@ -141,16 +148,18 @@ def test_rc_instance_return_conditional_values(self):
             'version': '',
             'etag': '123'
         }
-        server_template = remote_config.ServerTemplate(self._DEFAULT_APP, default_config)
-        server_template.set(remote_config.ServerTemplateData('etag', template_data))
+        server_template = remote_config.init_server_template(
+            app=self.mock_app,
+            default_config=default_config,
+            template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
+        )
+
         server_config = server_template.evaluate()
         assert server_config.get_boolean('is_enabled')
+        self.tear_down()
 
     def test_rc_instance_return_conditional_values_true(self):
-        recorder = []
-        self._RC_INSTANCE._client.session.mount(
-            'https://firebaseremoteconfig.googleapis.com',
-            MockAdapter(self._DEFAULT_RESPONSE, 200, recorder))
+        self.set_up()
         default_config = {'param1': 'in_app_default_param1', 'param3': 'in_app_default_param3'}
         condition = {
             'name': 'is_true',
@@ -184,16 +193,18 @@ def test_rc_instance_return_conditional_values_true(self):
             'version': '',
             'etag': '123'
         }
-        server_template = remote_config.ServerTemplate(self._DEFAULT_APP, default_config)
-        server_template.set(remote_config.ServerTemplateData('etag', template_data))
+        server_template = remote_config.init_server_template(
+            app=self.mock_app,
+            default_config=default_config,
+            template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
+        )
         server_config = server_template.evaluate()
         assert server_config.get_boolean('is_enabled')
+        self.tear_down()
+
 
     def test_rc_instance_return_conditional_values_honor_order(self):
-        recorder = []
-        self._RC_INSTANCE._client.session.mount(
-            'https://firebaseremoteconfig.googleapis.com',
-            MockAdapter(self._DEFAULT_RESPONSE, 200, recorder))
+        self.set_up()
         default_config = {'param1': 'in_app_default_param1', 'param3': 'in_app_default_param3'}
         template_data = {
             'conditions': [
@@ -249,16 +260,17 @@ def test_rc_instance_return_conditional_values_honor_order(self):
             'version':'',
             'etag': '123'
         }
-        server_template = remote_config.ServerTemplate(self._DEFAULT_APP, default_config)
-        server_template.set(remote_config.ServerTemplateData('etag', template_data))
+        server_template = remote_config.init_server_template(
+            app=self.mock_app,
+            default_config=default_config,
+            template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
+        )
         server_config = server_template.evaluate()
         assert server_config.get_string('dog_type') == 'corgi'
+        self.tear_down()
 
     def test_rc_instance_return_conditional_values_honor_order_final(self):
-        recorder = []
-        self._RC_INSTANCE._client.session.mount(
-            'https://firebaseremoteconfig.googleapis.com',
-            MockAdapter(self._DEFAULT_RESPONSE, 200, recorder))
+        self.set_up()
         default_config = {'param1': 'in_app_default_param1', 'param3': 'in_app_default_param3'}
         template_data = {
             'conditions': [
@@ -314,41 +326,48 @@ def test_rc_instance_return_conditional_values_honor_order_final(self):
             'version':'',
             'etag': '123'
         }
-        server_template = remote_config.ServerTemplate(self._DEFAULT_APP, default_config)
-        server_template.set(remote_config.ServerTemplateData('etag', template_data))
+        server_template = remote_config.init_server_template(
+            app=self.mock_app,
+            default_config=default_config,
+            template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
+        )
         server_config = server_template.evaluate()
         assert server_config.get_string('dog_type') == 'corgi'
+        self.tear_down()
 
     def test_rc_instance_evaluate_default_when_no_param(self):
-        recorder = []
-        self._RC_INSTANCE._client.session.mount(
-            'https://firebaseremoteconfig.googleapis.com',
-            MockAdapter(self._DEFAULT_RESPONSE, 200, recorder))
+        self.set_up()
         default_config = {'promo_enabled': False, 'promo_discount': 20,}
         template_data = SERVER_REMOTE_CONFIG_RESPONSE
         template_data['parameters'] = {}
-        server_template = remote_config.ServerTemplate(self._DEFAULT_APP, default_config)
-        server_template.set(remote_config.ServerTemplateData('etag', template_data))
+        server_template = remote_config.init_server_template(
+            app=self.mock_app,
+            default_config=default_config,
+            template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
+        )
         server_config = server_template.evaluate()
         assert server_config.get_boolean('promo_enabled') == default_config.get('promo_enabled')
         assert server_config.get_int('promo_discount') == default_config.get('promo_discount')
+        self.tear_down()
 
     def test_rc_instance_evaluate_default_when_no_default_value(self):
-        recorder = []
-        self._RC_INSTANCE._client.session.mount(
-            'https://firebaseremoteconfig.googleapis.com',
-            MockAdapter(self._DEFAULT_RESPONSE, 200, recorder))
+        self.set_up()
         default_config = {'default_value': 'local default'}
         template_data = SERVER_REMOTE_CONFIG_RESPONSE
         template_data['parameters'] = {
             'default_value': {}
         }
-        server_template = remote_config.ServerTemplate(self._DEFAULT_APP, default_config)
-        server_template.set(remote_config.ServerTemplateData('etag', template_data))
+        server_template = remote_config.init_server_template(
+            app=self.mock_app,
+            default_config=default_config,
+            template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
+        )
         server_config = server_template.evaluate()
         assert server_config.get_string('default_value') == default_config.get('default_value')
+        self.tear_down()
 
     def test_rc_instance_evaluate_default_when_in_default(self):
+        self.set_up()
         template_data = SERVER_REMOTE_CONFIG_RESPONSE
         template_data['parameters'] = {
             'remote_default_value': {}
@@ -356,44 +375,64 @@ def test_rc_instance_evaluate_default_when_in_default(self):
         default_config = {
             'inapp_default': '🐕'
         }
-        server_template = remote_config.ServerTemplate(self._DEFAULT_APP, default_config)
-        server_template.set(remote_config.ServerTemplateData('etag', template_data))
+        server_template = remote_config.init_server_template(
+            app=self.mock_app,
+            default_config=default_config,
+            template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
+        )
         server_config = server_template.evaluate()
         assert server_config.get_string('inapp_default') == default_config.get('inapp_default')
+        self.tear_down()
 
     def test_rc_instance_evaluate_default_when_defined(self):
+        self.set_up()
         template_data = SERVER_REMOTE_CONFIG_RESPONSE
         template_data['parameters'] = {}
         default_config = {
             'dog_type': 'shiba'
         }
-        server_template = remote_config.ServerTemplate(self._DEFAULT_APP, default_config)
-        server_template.set(remote_config.ServerTemplateData('etag', template_data))
+        server_template = remote_config.init_server_template(
+            app=self.mock_app,
+            default_config=default_config,
+            template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
+        )
         server_config = server_template.evaluate()
         assert server_config.get_value('dog_type').as_string() == 'shiba'
         assert server_config.get_value('dog_type').get_source() == 'default'
+        self.tear_down()
 
     def test_rc_instance_evaluate_return_numeric_value(self):
+        self.set_up()
         template_data = SERVER_REMOTE_CONFIG_RESPONSE
         default_config = {
             'dog_age': 12
         }
-        server_template = remote_config.ServerTemplate(self._DEFAULT_APP, default_config)
-        server_template.set(remote_config.ServerTemplateData('etag', template_data))
+        server_template = remote_config.init_server_template(
+            app=self.mock_app,
+            default_config=default_config,
+            template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
+        )
         server_config = server_template.evaluate()
         assert server_config.get_int('dog_age') == 12
+        self.tear_down()
 
     def test_rc_instance_evaluate_return__value(self):
+        self.set_up()
         template_data = SERVER_REMOTE_CONFIG_RESPONSE
         default_config = {
             'dog_is_cute': True
         }
-        server_template = remote_config.ServerTemplate(self._DEFAULT_APP, default_config)
-        server_template.set(remote_config.ServerTemplateData('etag', template_data))
+        server_template = remote_config.init_server_template(
+            app=self.mock_app,
+            default_config=default_config,
+            template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
+        )
         server_config = server_template.evaluate()
         assert server_config.get_int('dog_is_cute')
+        self.tear_down()
 
     def test_rc_instance_evaluate_unknown_operator_false(self):
+        self.set_up()
         condition = {
             'name': 'is_true',
             'condition': {
@@ -426,12 +465,17 @@ def test_rc_instance_evaluate_unknown_operator_false(self):
             'etag': '123'
         }
         context = {'randomization_id': '123'}
-        server_template = remote_config.ServerTemplate(self._DEFAULT_APP, default_config)
-        server_template.set(remote_config.ServerTemplateData('etag', template_data))
+        server_template = remote_config.init_server_template(
+            app=self.mock_app,
+            default_config=default_config,
+            template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
+        )
         server_config = server_template.evaluate(context)
         assert not server_config.get_boolean('is_enabled')
+        self.tear_down()
 
     def test_rc_instance_evaluate_less_max_equal_true(self):
+        self.set_up()
         condition = {
             'name': 'is_true',
             'condition': {
@@ -466,12 +510,17 @@ def test_rc_instance_evaluate_less_max_equal_true(self):
             'etag': '123'
         }
         context = {'randomization_id': '123'}
-        server_template = remote_config.ServerTemplate(self._DEFAULT_APP, default_config)
-        server_template.set(remote_config.ServerTemplateData('etag', template_data))
+        server_template = remote_config.init_server_template(
+            app=self.mock_app,
+            default_config=default_config,
+            template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
+        )
         server_config = server_template.evaluate(context)
         assert server_config.get_boolean('is_enabled')
+        self.tear_down()
 
     def test_rc_instance_evaluate_min_max_equal_true(self):
+        self.set_up()
         condition = {
             'name': 'is_true',
             'condition': {
@@ -509,12 +558,17 @@ def test_rc_instance_evaluate_min_max_equal_true(self):
             'etag': '123'
         }
         context = {'randomization_id': '123'}
-        server_template = remote_config.ServerTemplate(self._DEFAULT_APP, default_config)
-        server_template.set(remote_config.ServerTemplateData('etag', template_data))
+        server_template = remote_config.init_server_template(
+            app=self.mock_app,
+            default_config=default_config,
+            template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
+        )
         server_config = server_template.evaluate(context)
         assert server_config.get_boolean('is_enabled')
+        self.tear_down()
 
     def test_rc_instance_evaluate_min_max_equal_false(self):
+        self.set_up()
         condition = {
             'name': 'is_true',
             'condition': {
@@ -552,12 +606,17 @@ def test_rc_instance_evaluate_min_max_equal_false(self):
             'etag': '123'
         }
         context = {'randomization_id': '123'}
-        server_template = remote_config.ServerTemplate(self._DEFAULT_APP, default_config)
-        server_template.set(remote_config.ServerTemplateData('etag', template_data))
+        server_template = remote_config.init_server_template(
+            app=self.mock_app,
+            default_config=default_config,
+            template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
+        )
         server_config = server_template.evaluate(context)
         assert not server_config.get_boolean('is_enabled')
+        self.tear_down()
 
     def test_rc_instance_evaluate_less_or_equal_approx(self):
+        self.set_up()
         condition = {
             'name': 'is_true',
             'condition': {
@@ -580,13 +639,15 @@ def test_rc_instance_evaluate_less_or_equal_approx(self):
             'dog_is_cute': True
         }
 
-        server_template = remote_config.ServerTemplate(self._DEFAULT_APP, default_config)
-        truthy_assignments = self.evaluate_random_assignments(condition, 100000, server_template)
+        truthy_assignments = self.evaluate_random_assignments(condition, 100000,
+                                                              self.mock_app, default_config)
         tolerance = 284
         assert truthy_assignments >= 10000 - tolerance
         assert truthy_assignments <= 10000 + tolerance
+        self.tear_down()
 
     def test_rc_instance_evaluate_between_approx(self):
+        self.set_up()
         condition = {
             'name': 'is_true',
             'condition': {
@@ -612,13 +673,15 @@ def test_rc_instance_evaluate_between_approx(self):
             'dog_is_cute': True
         }
 
-        server_template = remote_config.ServerTemplate(self._DEFAULT_APP, default_config)
-        truthy_assignments = self.evaluate_random_assignments(condition, 100000, server_template)
+        truthy_assignments = self.evaluate_random_assignments(condition, 100000,
+                                                              self.mock_app, default_config)
         tolerance = 379
         assert truthy_assignments >= 20000 - tolerance
         assert truthy_assignments <= 20000 + tolerance
+        self.tear_down()
 
     def test_rc_instance_evaluate_between_interquartile_range_approx(self):
+        self.set_up()
         condition = {
             'name': 'is_true',
             'condition': {
@@ -644,13 +707,14 @@ def test_rc_instance_evaluate_between_interquartile_range_approx(self):
             'dog_is_cute': True
         }
 
-        server_template = remote_config.ServerTemplate(self._DEFAULT_APP, default_config)
-        truthy_assignments = self.evaluate_random_assignments(condition, 100000, server_template)
+        truthy_assignments = self.evaluate_random_assignments(condition, 100000,
+                                                              self.mock_app, default_config)
         tolerance = 474
         assert truthy_assignments >= 50000 - tolerance
         assert truthy_assignments <= 50000 + tolerance
+        self.tear_down()
 
-    def evaluate_random_assignments(self, condition, num_of_assignments, server_template) -> int:
+    def evaluate_random_assignments(self, condition, num_of_assignments, mock_app, default_config) -> int:
         """Evaluates random assignments based on a condition.
 
         Args:
@@ -674,7 +738,12 @@ def evaluate_random_assignments(self, condition, num_of_assignments, server_temp
             'version':'',
             'etag': '123'
         }
-        server_template.set(remote_config.ServerTemplateData('etag', template_data))
+        server_template = remote_config.init_server_template(
+            app=mock_app,
+            default_config=default_config,
+            template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
+        )
+
         for _ in range(num_of_assignments):
             context = {'randomization_id': str(uuid.uuid4())}
             result = server_template.evaluate(context)

From 5c355402f536a7dbaca60fc5dbb5328301406f59 Mon Sep 17 00:00:00 2001
From: Varun Rathore <varunrathore@google.com>
Date: Mon, 4 Nov 2024 19:18:47 +0530
Subject: [PATCH 08/15] resolve lint comments

---
 tests/test_remote_config.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tests/test_remote_config.py b/tests/test_remote_config.py
index a60d4998e..e702b27d5 100644
--- a/tests/test_remote_config.py
+++ b/tests/test_remote_config.py
@@ -714,7 +714,7 @@ def test_rc_instance_evaluate_between_interquartile_range_approx(self):
         assert truthy_assignments <= 50000 + tolerance
         self.tear_down()
 
-    def evaluate_random_assignments(self, condition, num_of_assignments, mock_app, default_config) -> int:
+    def evaluate_random_assignments(self, condition, num_of_assignments, mock_app, default_config):
         """Evaluates random assignments based on a condition.
 
         Args:

From 55f2a0a2bd047f56654428019c8b9a078e86dc2e Mon Sep 17 00:00:00 2001
From: Varun Rathore <varunrathore@google.com>
Date: Wed, 6 Nov 2024 17:15:31 +0530
Subject: [PATCH 09/15] Fixed bug

---
 firebase_admin/_http_client.py  |   8 --
 firebase_admin/remote_config.py | 232 +++++++++++++++++++-------------
 tests/test_remote_config.py     |   2 +-
 3 files changed, 139 insertions(+), 103 deletions(-)

diff --git a/firebase_admin/_http_client.py b/firebase_admin/_http_client.py
index ab6c6b954..d259faddf 100644
--- a/firebase_admin/_http_client.py
+++ b/firebase_admin/_http_client.py
@@ -148,11 +148,3 @@ def __init__(self, **kwargs):
 
     def parse_body(self, resp):
         return resp.json()
-
-class RemoteConfigApiClient(HttpClient):
-    """An HTTP client that parses response messages as JSON."""
-    def __init__(self, **kwargs):
-        HttpClient.__init__(self, **kwargs)
-    def parse_body(self, resp):
-        return resp.json()
-        
\ No newline at end of file
diff --git a/firebase_admin/remote_config.py b/firebase_admin/remote_config.py
index 25e8125cb..618264ce7 100644
--- a/firebase_admin/remote_config.py
+++ b/firebase_admin/remote_config.py
@@ -18,7 +18,7 @@
 
 import json
 import logging
-from typing import Dict, Optional, Literal, Callable, Union
+from typing import Dict, Optional, Literal, Union
 from enum import Enum
 import re
 import hashlib
@@ -228,15 +228,14 @@ def evaluate(self):
         evaluated_conditions = self.evaluate_conditions(self._conditions, self._context)
 
         # Overlays config Value objects derived by evaluating the template.
-       # evaluated_conditions = None
-        if self._parameters is not None:
+        if self._parameters:
             for key, parameter in self._parameters.items():
                 conditional_values = parameter.get('conditionalValues', {})
                 default_value = parameter.get('defaultValue', {})
                 parameter_value_wrapper = None
                 # Iterates in order over condition list. If there is a value associated
                 # with a condition, this checks if the condition is true.
-                if evaluated_conditions is not None:
+                if evaluated_conditions:
                     for condition_name, condition_evaluation in evaluated_conditions.items():
                         if condition_name in conditional_values and condition_evaluation:
                             parameter_value_wrapper = conditional_values[condition_name]
@@ -404,6 +403,7 @@ def hash_seeded_randomization_id(self, seeded_randomization_id: str) -> int:
         hash_object.update(seeded_randomization_id.encode('utf-8'))
         hash64 = hash_object.hexdigest()
         return abs(int(hash64, 16))
+
     def evaluate_custom_signal_condition(self, custom_signal_condition,
                                          context) -> bool:
         """Evaluates a custom signal condition.
@@ -417,124 +417,168 @@ def evaluate_custom_signal_condition(self, custom_signal_condition,
         """
         custom_signal_operator = custom_signal_condition.get('custom_signal_operator') or {}
         custom_signal_key = custom_signal_condition.get('custom_signal_key') or {}
-        tgt_custom_signal_values = custom_signal_condition.get('target_custom_signal_values') or {}
+        target_custom_signal_values = (
+            custom_signal_condition.get('target_custom_signal_values') or {})
 
-        if not all([custom_signal_operator, custom_signal_key, tgt_custom_signal_values]):
+        if not all([custom_signal_operator, custom_signal_key, target_custom_signal_values]):
             logger.warning("Missing operator, key, or target values for custom signal condition.")
             return False
 
-        if not tgt_custom_signal_values:
+        if not target_custom_signal_values:
             return False
-        actual_custom_signal_value = getattr(context, custom_signal_key, None)
-        if actual_custom_signal_value is None:
+        actual_custom_signal_value = context.get(custom_signal_key) or {}
+
+        if not actual_custom_signal_value:
             logger.warning("Custom signal value not found in context: %s", custom_signal_key)
             return False
+
         if custom_signal_operator == CustomSignalOperator.STRING_CONTAINS:
-            return compare_strings(lambda target, actual: target in actual)
+            return self._compare_strings(target_custom_signal_values,
+                                         actual_custom_signal_value,
+                                         lambda target, actual: target in actual)
         if custom_signal_operator == CustomSignalOperator.STRING_DOES_NOT_CONTAIN:
-            return not compare_strings(lambda target, actual: target in actual)
+            return not self._compare_strings(target_custom_signal_values,
+                                             actual_custom_signal_value,
+                                             lambda target, actual: target in actual)
         if custom_signal_operator == CustomSignalOperator.STRING_EXACTLY_MATCHES:
-            return compare_strings(lambda target, actual: target.strip() == actual.strip())
+            return self._compare_strings(target_custom_signal_values,
+                                         actual_custom_signal_value,
+                                         lambda target, actual: target.strip() == actual.strip())
         if custom_signal_operator == CustomSignalOperator.STRING_CONTAINS_REGEX:
-            return compare_strings(lambda target, actual: re.search(target, actual) is not None)
+            return self._compare_strings(target_custom_signal_values,
+                                         actual_custom_signal_value,
+                                         re.search)
+
+        # For numeric operators only one target value is allowed.
         if custom_signal_operator == CustomSignalOperator.NUMERIC_LESS_THAN:
-            return compare_numbers(lambda r: r < 0)
+            return self._compare_numbers(target_custom_signal_values[0],
+                                         actual_custom_signal_value,
+                                         lambda r: r < 0)
         if custom_signal_operator == CustomSignalOperator.NUMERIC_LESS_EQUAL:
-            return compare_numbers(lambda r: r <= 0)
+            return self._compare_numbers(target_custom_signal_values[0],
+                                         actual_custom_signal_value,
+                                         lambda r: r <= 0)
         if custom_signal_operator == CustomSignalOperator.NUMERIC_EQUAL:
-            return compare_numbers(lambda r: r == 0)
+            return self._compare_numbers(target_custom_signal_values[0],
+                                         actual_custom_signal_value,
+                                         lambda r: r == 0)
         if custom_signal_operator == CustomSignalOperator.NUMERIC_NOT_EQUAL:
-            return compare_numbers(lambda r: r != 0)
+            return self._compare_numbers(target_custom_signal_values[0],
+                                         actual_custom_signal_value,
+                                         lambda r: r != 0)
         if custom_signal_operator == CustomSignalOperator.NUMERIC_GREATER_THAN:
-            return compare_numbers(lambda r: r > 0)
+            return self._compare_numbers(target_custom_signal_values[0],
+                                         actual_custom_signal_value,
+                                         lambda r: r > 0)
         if custom_signal_operator == CustomSignalOperator.NUMERIC_GREATER_EQUAL:
-            return compare_numbers(lambda r: r >= 0)
+            return self._compare_numbers(target_custom_signal_values[0],
+                                         actual_custom_signal_value,
+                                         lambda r: r >= 0)
+
+        # For semantic operators only one target value is allowed.
         if custom_signal_operator == CustomSignalOperator.SEMANTIC_VERSION_LESS_THAN:
-            return compare_semantic_versions(lambda r: r < 0)
+            return self._compare_semantic_versions(target_custom_signal_values[0],
+                                                   actual_custom_signal_value,
+                                                   lambda r: r < 0)
         if custom_signal_operator == CustomSignalOperator.SEMANTIC_VERSION_LESS_EQUAL:
-            return compare_semantic_versions(lambda r: r <= 0)
+            return self._compare_semantic_versions(target_custom_signal_values[0],
+                                                   actual_custom_signal_value,
+                                                   lambda r: r <= 0)
         if custom_signal_operator == CustomSignalOperator.SEMANTIC_VERSION_EQUAL:
-            return compare_semantic_versions(lambda r: r == 0)
+            return self._compare_semantic_versions(target_custom_signal_values[0],
+                                                   actual_custom_signal_value,
+                                                   lambda r: r == 0)
         if custom_signal_operator == CustomSignalOperator.SEMANTIC_VERSION_NOT_EQUAL:
-            return compare_semantic_versions(lambda r: r != 0)
+            return self._compare_semantic_versions(target_custom_signal_values[0],
+                                                   actual_custom_signal_value,
+                                                   lambda r: r != 0)
         if custom_signal_operator == CustomSignalOperator.SEMANTIC_VERSION_GREATER_THAN:
-            return compare_semantic_versions(lambda r: r > 0)
+            return self._compare_semantic_versions(target_custom_signal_values[0],
+                                                   actual_custom_signal_value,
+                                                   lambda r: r > 0)
         if custom_signal_operator == CustomSignalOperator.SEMANTIC_VERSION_GREATER_EQUAL:
-            return compare_semantic_versions(lambda r: r >= 0)
-
-        def compare_strings(predicate_fn: Callable[[str, str], bool]) -> bool:
-            """Compares the actual string value of a signal against a list of target values.
-
-            Args:
-                predicate_fn: A function that takes two string arguments (target and actual)
-                                and returns a boolean indicating whether
-                                the target matches the actual value.
-
-            Returns:
-                bool: True if the predicate function returns True for any target value in the list,
-                    False otherwise.
-            """
-            for target in tgt_custom_signal_values:
-                if predicate_fn(target, str(actual_custom_signal_value)):
-                    return True
-            return False
+            return self._compare_semantic_versions(target_custom_signal_values[0],
+                                                   actual_custom_signal_value,
+                                                   lambda r: r >= 0)
+        logger.warning("Unknown custom signal operator: %s", custom_signal_operator)
+        return False
 
-        def compare_numbers(predicate_fn: Callable[[int], bool]) -> bool:
-            try:
-                target = float(tgt_custom_signal_values[0])
-                actual = float(actual_custom_signal_value)
-                result = -1 if actual < target else 1 if actual > target else 0
-                return predicate_fn(result)
-            except ValueError:
-                logger.warning("Invalid numeric value for comparison.")
-                return False
+    def _compare_strings(self, target_values, actual_value, predicate_fn) -> bool:
+        """Compares the actual string value of a signal against a list of target values.
 
-        def compare_semantic_versions(predicate_fn: Callable[[int], bool]) -> bool:
-            """Compares the actual semantic version value of a signal against a target value.
-            Calls the predicate function with -1, 0, 1 if actual is less than, equal to,
-            or greater than target.
-
-            Args:
-            predicate_fn: A function that takes an integer (-1, 0, or 1) and returns a boolean.
-
-            Returns:
-                bool: True if the predicate function returns True for the result of the comparison,
-            False otherwise.
-            """
-            return compare_versions(str(actual_custom_signal_value),
-                                    str(tgt_custom_signal_values[0]), predicate_fn)
-        def compare_versions(version1: str, version2: str,
-                             predicate_fn: Callable[[int], bool]) -> bool:
-            """Compares two semantic version strings.
-
-            Args:
-            version1: The first semantic version string.
-            version2: The second semantic version string.
-            predicate_fn: A function that takes an integer and returns a boolean.
-
-            Returns:
-                bool: The result of the predicate function.
-            """
-            try:
-                v1_parts = [int(part) for part in version1.split('.')]
-                v2_parts = [int(part) for part in version2.split('.')]
-                max_length = max(len(v1_parts), len(v2_parts))
-                v1_parts.extend([0] * (max_length - len(v1_parts)))
-                v2_parts.extend([0] * (max_length - len(v2_parts)))
-
-                for part1, part2 in zip(v1_parts, v2_parts):
-                    if part1 < part2:
-                        return predicate_fn(-1)
-                    if part1 > part2:
-                        return predicate_fn(1)
-                return predicate_fn(0)
-            except ValueError:
-                logger.warning("Invalid semantic version format for comparison.")
-                return False
+        Args:
+            target_values: A list of target string values.
+            actual_value: The actual value to compare, which can be a string or number.
+            predicate_fn: A function that takes two string arguments (target and actual)
+                            and returns a boolean indicating whether
+                            the target matches the actual value.
 
-        logger.warning("Unknown custom signal operator: %s", custom_signal_operator)
+        Returns:
+            bool: True if the predicate function returns True for any target value in the list,
+                False otherwise.
+        """
+
+        for target in target_values:
+            if predicate_fn(target, str(actual_value)):
+                return True
         return False
 
+    def _compare_numbers(self, target_value, actual_value, predicate_fn) -> bool:
+        try:
+            target = float(target_value)
+            actual = float(actual_value)
+            result = -1 if actual < target else 1 if actual > target else 0
+            return predicate_fn(result)
+        except ValueError:
+            logger.warning("Invalid numeric value for comparison.")
+            return False
+
+    def _compare_semantic_versions(self, target_value, actual_value, predicate_fn) -> bool:
+        """Compares the actual semantic version value of a signal against a target value.
+        Calls the predicate function with -1, 0, 1 if actual is less than, equal to,
+        or greater than target.
+
+        Args:
+        target_values: A list of target string values.
+        actual_value: The actual value to compare, which can be a string or number.
+        predicate_fn: A function that takes an integer (-1, 0, or 1) and returns a boolean.
+
+        Returns:
+            bool: True if the predicate function returns True for the result of the comparison,
+        False otherwise.
+        """
+        return self._compare_versions(str(actual_value),
+                                      str(target_value), predicate_fn)
+
+    def _compare_versions(self, version1, version2, predicate_fn) -> bool:
+        """Compares two semantic version strings.
+
+        Args:
+        version1: The first semantic version string.
+        version2: The second semantic version string.
+        predicate_fn: A function that takes an integer and returns a boolean.
+
+        Returns:
+            bool: The result of the predicate function.
+        """
+        try:
+            v1_parts = [int(part) for part in version1.split('.')]
+            v2_parts = [int(part) for part in version2.split('.')]
+            max_length = max(len(v1_parts), len(v2_parts))
+            v1_parts.extend([0] * (max_length - len(v1_parts)))
+            v2_parts.extend([0] * (max_length - len(v2_parts)))
+
+            for part1, part2 in zip(v1_parts, v2_parts):
+                if part1 < part2:
+                    return predicate_fn(-1)
+                if part1 > part2:
+                    return predicate_fn(1)
+            return predicate_fn(0)
+        except ValueError:
+            logger.warning("Invalid semantic version format for comparison.")
+            return False
+
+
 async def get_server_template(app: App = None, default_config: Optional[Dict[str, str]] = None):
     """Initializes a new ServerTemplate instance and fetches the server template.
 
diff --git a/tests/test_remote_config.py b/tests/test_remote_config.py
index e702b27d5..66e2225d8 100644
--- a/tests/test_remote_config.py
+++ b/tests/test_remote_config.py
@@ -680,7 +680,7 @@ def test_rc_instance_evaluate_between_approx(self):
         assert truthy_assignments <= 20000 + tolerance
         self.tear_down()
 
-    def test_rc_instance_evaluate_between_interquartile_range_approx(self):
+    def test_rc_instance_evaluate_between_interquartile_range_accuracy(self):
         self.set_up()
         condition = {
             'name': 'is_true',

From ad0e13c4442b1d238818835d319790f1f197081e Mon Sep 17 00:00:00 2001
From: Varun Rathore <varunrathore@google.com>
Date: Wed, 6 Nov 2024 21:14:44 +0530
Subject: [PATCH 10/15] Added fixes

---
 firebase_admin/remote_config.py |   8 +-
 tests/test_remote_config.py     | 235 +++++++++++++++++---------------
 2 files changed, 134 insertions(+), 109 deletions(-)

diff --git a/firebase_admin/remote_config.py b/firebase_admin/remote_config.py
index 618264ce7..0d92bbc52 100644
--- a/firebase_admin/remote_config.py
+++ b/firebase_admin/remote_config.py
@@ -377,15 +377,19 @@ def evaluate_percent_condition(self, percent_condition,
         else:
             norm_percent_upper_bound = 0
             norm_percent_lower_bound = 0
+        if micro_percent:
+            norm_micro_percent = micro_percent
+        else:
+            norm_micro_percent = 0
         seed_prefix = f"{seed}." if seed else ""
         string_to_hash = f"{seed_prefix}{context.get('randomization_id')}"
 
         hash64 = self.hash_seeded_randomization_id(string_to_hash)
         instance_micro_percentile = hash64 % (100 * 1000000)
         if percent_operator == PercentConditionOperator.LESS_OR_EQUAL:
-            return instance_micro_percentile <= micro_percent
+            return instance_micro_percentile <= norm_micro_percent
         if percent_operator == PercentConditionOperator.GREATER_THAN:
-            return instance_micro_percentile > micro_percent
+            return instance_micro_percentile > norm_micro_percent
         if percent_operator == PercentConditionOperator.BETWEEN:
             return norm_percent_lower_bound < instance_micro_percentile <= norm_percent_upper_bound
         logger.warning("Unknown percent operator: %s", percent_operator)
diff --git a/tests/test_remote_config.py b/tests/test_remote_config.py
index 66e2225d8..49aa86338 100644
--- a/tests/test_remote_config.py
+++ b/tests/test_remote_config.py
@@ -13,31 +13,25 @@
 # limitations under the License.
 
 """Tests for firebase_admin.remote_config."""
-import json
 import uuid
 from unittest import mock
 import firebase_admin
 from firebase_admin.remote_config import (
-    _REMOTE_CONFIG_ATTRIBUTE,
-    _RemoteConfigService,
     PercentConditionOperator,
     ServerTemplateData)
-from firebase_admin import remote_config, _utils
-from tests import testutils
-
-
-
+from firebase_admin import remote_config
 
 VERSION_INFO = {
     'versionNumber': '86',
-    'updateOrigin': 'ADMIN_SDK_NODE',
+    'updateOrigin': 'ADMIN_SDK_PYTHON',
     'updateType': 'INCREMENTAL_UPDATE',
     'updateUser': {
         'email': 'firebase-adminsdk@gserviceaccount.com'
     },
     'description': 'production version',
-    'updateTime': '2020-06-15T16:45:03.541527Z'
+    'updateTime': '2024-11-05T16:45:03.541527Z'
     }
+
 SERVER_REMOTE_CONFIG_RESPONSE = {
     'conditions': [
         {
@@ -68,34 +62,7 @@
     'version': VERSION_INFO,
     }
 
-class MockAdapter(testutils.MockAdapter):
-    """A Mock HTTP Adapter that Firebase Remote Config with ETag in header."""
-
-    ETAG = '0'
-
-    def __init__(self, data, status, recorder, etag=ETAG):
-        testutils.MockAdapter.__init__(self, data, status, recorder)
-        self._etag = etag
-
-    def send(self, request, **kwargs):
-        resp = super(MockAdapter, self).send(request, **kwargs)
-        resp.headers = {'ETag': self._etag}
-        return resp
-
-
-class TestGetServerTemplate:
-    _DEFAULT_APP = firebase_admin.initialize_app(testutils.MockCredential(), name='no_project_id')
-    _RC_INSTANCE = _utils.get_app_service(_DEFAULT_APP,
-                                          _REMOTE_CONFIG_ATTRIBUTE, _RemoteConfigService)
-    _DEFAULT_RESPONSE = json.dumps({
-        'parameters': {
-            'test_key': 'test_value'
-        },
-        'conditions': {},
-        'parameterGroups': {},
-        'version': 'test'
-        })
-
+class TestEvaluate:
     def set_up(self):
         # Create a more specific mock for firebase_admin.App
         self.mock_app = mock.create_autospec(firebase_admin.App)
@@ -113,7 +80,7 @@ def set_up(self):
     def tear_down(self):
         mock.patch.stopall()
 
-    def test_rc_instance_return_conditional_values(self):
+    def test_evaluate_or_and_true_condition_true(self):
         self.set_up()
         default_config = {'param1': 'in_app_default_param1', 'param3': 'in_app_default_param3'}
         condition = {
@@ -158,7 +125,7 @@ def test_rc_instance_return_conditional_values(self):
         assert server_config.get_boolean('is_enabled')
         self.tear_down()
 
-    def test_rc_instance_return_conditional_values_true(self):
+    def test_evaluate_or_and_false_condition_false(self):
         self.set_up()
         default_config = {'param1': 'in_app_default_param1', 'param3': 'in_app_default_param3'}
         condition = {
@@ -171,7 +138,7 @@ def test_rc_instance_return_conditional_values_true(self):
                                 'conditions': [
                                     {
                                         'name': '',
-                                        'true': {
+                                        'false': {
                                         }
                                     }
                                 ]
@@ -198,66 +165,31 @@ def test_rc_instance_return_conditional_values_true(self):
             default_config=default_config,
             template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
         )
+
         server_config = server_template.evaluate()
-        assert server_config.get_boolean('is_enabled')
+        assert not server_config.get_boolean('is_enabled')
         self.tear_down()
 
-
-    def test_rc_instance_return_conditional_values_honor_order(self):
+    def test_evaluate_non_or_condition(self):
         self.set_up()
         default_config = {'param1': 'in_app_default_param1', 'param3': 'in_app_default_param3'}
-        template_data = {
-            'conditions': [
-                {
-                    'name': 'is_true',
-                    'condition': {
-                        'orCondition': {
-                            'conditions': [
-                                {
-                                    'andCondition': {
-                                        'conditions': [
-                                            {
-                                                'true': {
-                                                }
-                                            }
-                                        ]
-                                    }
-                                }
-                            ]
-                        }
-                    }
-                },
-                {
-                    'name': 'is_true_too',
-                    'condition': {
-                        'orCondition': {
-                            'conditions': [
-                                {
-                                    'andCondition': {
-                                        'conditions': [
-                                            {
-                                                'true': {
-                                                }
-                                            }
-                                        ]
-                                    }
-                                }
-                            ]
-                        }
-                    }
+        condition = {
+            'name': 'is_true',
+            'condition': {
+                'true': {
                 }
-            ],
+            }
+        }
+        template_data = {
+            'conditions': [condition],
             'parameters': {
-                'dog_type': {
-                    'defaultValue': {'value': 'chihuahua'},
-                    'conditionalValues': {
-                        'is_true_too': {'value': 'dachshund'},
-                        'is_true': {'value': 'corgi'}
-                    }
+                'is_enabled': {
+                    'defaultValue': {'value': 'false'},
+                    'conditionalValues': {'is_true': {'value': 'true'}}
                 },
             },
-            'parameterGroups':'',
-            'version':'',
+            'parameterGroups': '',
+            'version': '',
             'etag': '123'
         }
         server_template = remote_config.init_server_template(
@@ -265,11 +197,12 @@ def test_rc_instance_return_conditional_values_honor_order(self):
             default_config=default_config,
             template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
         )
+
         server_config = server_template.evaluate()
-        assert server_config.get_string('dog_type') == 'corgi'
+        assert server_config.get_boolean('is_enabled')
         self.tear_down()
 
-    def test_rc_instance_return_conditional_values_honor_order_final(self):
+    def test_evaluate_return_conditional_values_honor_order(self):
         self.set_up()
         default_config = {'param1': 'in_app_default_param1', 'param3': 'in_app_default_param3'}
         template_data = {
@@ -335,7 +268,7 @@ def test_rc_instance_return_conditional_values_honor_order_final(self):
         assert server_config.get_string('dog_type') == 'corgi'
         self.tear_down()
 
-    def test_rc_instance_evaluate_default_when_no_param(self):
+    def test_evaluate_default_when_no_param(self):
         self.set_up()
         default_config = {'promo_enabled': False, 'promo_discount': 20,}
         template_data = SERVER_REMOTE_CONFIG_RESPONSE
@@ -350,7 +283,7 @@ def test_rc_instance_evaluate_default_when_no_param(self):
         assert server_config.get_int('promo_discount') == default_config.get('promo_discount')
         self.tear_down()
 
-    def test_rc_instance_evaluate_default_when_no_default_value(self):
+    def test_evaluate_default_when_no_default_value(self):
         self.set_up()
         default_config = {'default_value': 'local default'}
         template_data = SERVER_REMOTE_CONFIG_RESPONSE
@@ -366,7 +299,7 @@ def test_rc_instance_evaluate_default_when_no_default_value(self):
         assert server_config.get_string('default_value') == default_config.get('default_value')
         self.tear_down()
 
-    def test_rc_instance_evaluate_default_when_in_default(self):
+    def test_evaluate_default_when_in_default(self):
         self.set_up()
         template_data = SERVER_REMOTE_CONFIG_RESPONSE
         template_data['parameters'] = {
@@ -384,7 +317,7 @@ def test_rc_instance_evaluate_default_when_in_default(self):
         assert server_config.get_string('inapp_default') == default_config.get('inapp_default')
         self.tear_down()
 
-    def test_rc_instance_evaluate_default_when_defined(self):
+    def test_evaluate_default_when_defined(self):
         self.set_up()
         template_data = SERVER_REMOTE_CONFIG_RESPONSE
         template_data['parameters'] = {}
@@ -401,7 +334,7 @@ def test_rc_instance_evaluate_default_when_defined(self):
         assert server_config.get_value('dog_type').get_source() == 'default'
         self.tear_down()
 
-    def test_rc_instance_evaluate_return_numeric_value(self):
+    def test_evaluate_return_numeric_value(self):
         self.set_up()
         template_data = SERVER_REMOTE_CONFIG_RESPONSE
         default_config = {
@@ -416,7 +349,7 @@ def test_rc_instance_evaluate_return_numeric_value(self):
         assert server_config.get_int('dog_age') == 12
         self.tear_down()
 
-    def test_rc_instance_evaluate_return__value(self):
+    def test_evaluate_return__value(self):
         self.set_up()
         template_data = SERVER_REMOTE_CONFIG_RESPONSE
         default_config = {
@@ -431,7 +364,7 @@ def test_rc_instance_evaluate_return__value(self):
         assert server_config.get_int('dog_is_cute')
         self.tear_down()
 
-    def test_rc_instance_evaluate_unknown_operator_false(self):
+    def test_evaluate_unknown_operator_to_false(self):
         self.set_up()
         condition = {
             'name': 'is_true',
@@ -474,7 +407,7 @@ def test_rc_instance_evaluate_unknown_operator_false(self):
         assert not server_config.get_boolean('is_enabled')
         self.tear_down()
 
-    def test_rc_instance_evaluate_less_max_equal_true(self):
+    def test_evaluate_less_or_equal_to_max_to_true(self):
         self.set_up()
         condition = {
             'name': 'is_true',
@@ -519,7 +452,95 @@ def test_rc_instance_evaluate_less_max_equal_true(self):
         assert server_config.get_boolean('is_enabled')
         self.tear_down()
 
-    def test_rc_instance_evaluate_min_max_equal_true(self):
+    def test_evaluate_undefined_micropercent_to_false(self):
+        self.set_up()
+        condition = {
+            'name': 'is_true',
+            'condition': {
+                'orCondition': {
+                    'conditions': [{
+                        'andCondition': {
+                            'conditions': [{
+                                'percent': {
+                                    'percentOperator': PercentConditionOperator.LESS_OR_EQUAL,
+                                    # Leaves microPercent undefined
+                                }
+                            }],
+                        }
+                    }]
+                }
+            }
+        }
+        default_config = {
+            'dog_is_cute': True
+        }
+        template_data = {
+            'conditions': [condition],
+            'parameters': {
+                'is_enabled': {
+                    'defaultValue': {'value': 'false'},
+                    'conditionalValues': {'is_true': {'value': 'true'}}
+                },
+            },
+            'parameterGroups':'',
+            'version':'',
+            'etag': '123'
+        }
+        context = {'randomization_id': '123'}
+        server_template = remote_config.init_server_template(
+            app=self.mock_app,
+            default_config=default_config,
+            template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
+        )
+        server_config = server_template.evaluate(context)
+        assert not server_config.get_boolean('is_enabled')
+        self.tear_down()
+
+    def test_evaluate_undefined_micropercentrange_to_false(self):
+        self.set_up()
+        condition = {
+            'name': 'is_true',
+            'condition': {
+                'orCondition': {
+                    'conditions': [{
+                        'andCondition': {
+                            'conditions': [{
+                                'percent': {
+                                    'percentOperator': PercentConditionOperator.BETWEEN,
+                                    # Leaves microPercent undefined
+                                }
+                            }],
+                        }
+                    }]
+                }
+            }
+        }
+        default_config = {
+            'dog_is_cute': True
+        }
+        template_data = {
+            'conditions': [condition],
+            'parameters': {
+                'is_enabled': {
+                    'defaultValue': {'value': 'false'},
+                    'conditionalValues': {'is_true': {'value': 'true'}}
+                },
+            },
+            'parameterGroups':'',
+            'version':'',
+            'etag': '123'
+        }
+        context = {'randomization_id': '123'}
+        server_template = remote_config.init_server_template(
+            app=self.mock_app,
+            default_config=default_config,
+            template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
+        )
+        server_config = server_template.evaluate(context)
+        assert not server_config.get_boolean('is_enabled')
+        self.tear_down()
+
+    def test_evaluate_between_min_max_to_true(self):
         self.set_up()
         condition = {
             'name': 'is_true',
@@ -567,7 +588,7 @@ def test_rc_instance_evaluate_min_max_equal_true(self):
         assert server_config.get_boolean('is_enabled')
         self.tear_down()
 
-    def test_rc_instance_evaluate_min_max_equal_false(self):
+    def test_evaluate_between_equal_bounds_to_false(self):
         self.set_up()
         condition = {
             'name': 'is_true',
@@ -615,7 +636,7 @@ def test_rc_instance_evaluate_min_max_equal_false(self):
         assert not server_config.get_boolean('is_enabled')
         self.tear_down()
 
-    def test_rc_instance_evaluate_less_or_equal_approx(self):
+    def test_evaluate_less_or_equal_to_approx(self):
         self.set_up()
         condition = {
             'name': 'is_true',
@@ -646,7 +667,7 @@ def test_rc_instance_evaluate_less_or_equal_approx(self):
         assert truthy_assignments <= 10000 + tolerance
         self.tear_down()
 
-    def test_rc_instance_evaluate_between_approx(self):
+    def test_evaluate_between_approx(self):
         self.set_up()
         condition = {
             'name': 'is_true',
@@ -680,7 +701,7 @@ def test_rc_instance_evaluate_between_approx(self):
         assert truthy_assignments <= 20000 + tolerance
         self.tear_down()
 
-    def test_rc_instance_evaluate_between_interquartile_range_accuracy(self):
+    def test_evaluate_between_interquartile_range_accuracy(self):
         self.set_up()
         condition = {
             'name': 'is_true',

From 9ff9600539133c744f1b0c9b915f21bc41c5f318 Mon Sep 17 00:00:00 2001
From: Varun Rathore <varunrathore@google.com>
Date: Wed, 6 Nov 2024 21:19:01 +0530
Subject: [PATCH 11/15] Added fixe

---
 tests/testutils.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/tests/testutils.py b/tests/testutils.py
index 12c413989..e2f58d5d7 100644
--- a/tests/testutils.py
+++ b/tests/testutils.py
@@ -234,6 +234,7 @@ def build_mock_parameter(name, description, value=None,
         'value': value,
         'conditionalValues': conditional_values,
         'defaultValue': default_value,
+        'parameterGroups': parameter_groups,
         # ... other relevant fields ...
     }
 

From 4cd1eb14b2fbebb8871e6e92a89c9556014262e5 Mon Sep 17 00:00:00 2001
From: Varun Rathore <varunrathore@google.com>
Date: Wed, 6 Nov 2024 21:22:10 +0530
Subject: [PATCH 12/15] Added fix for lint

---
 tests/testutils.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tests/testutils.py b/tests/testutils.py
index e2f58d5d7..3893a3463 100644
--- a/tests/testutils.py
+++ b/tests/testutils.py
@@ -227,7 +227,7 @@ def build_mock_condition(name, condition):
     }
 
 def build_mock_parameter(name, description, value=None,
-                         conditional_values=None, default_value=None):
+                         conditional_values=None, default_value=None, parameter_groups=None):
     return {
         'name': name,
         'description': description,

From 11bb8dc7fc8d11fe5a05d80380366f10804866fd Mon Sep 17 00:00:00 2001
From: Varun Rathore <varunrathore@google.com>
Date: Thu, 7 Nov 2024 19:46:44 +0530
Subject: [PATCH 13/15] Changed structure of test

---
 tests/test_remote_config.py | 119 ++++++++++++++----------------------
 1 file changed, 46 insertions(+), 73 deletions(-)

diff --git a/tests/test_remote_config.py b/tests/test_remote_config.py
index 49aa86338..5cba92ce5 100644
--- a/tests/test_remote_config.py
+++ b/tests/test_remote_config.py
@@ -14,12 +14,12 @@
 
 """Tests for firebase_admin.remote_config."""
 import uuid
-from unittest import mock
 import firebase_admin
 from firebase_admin.remote_config import (
     PercentConditionOperator,
     ServerTemplateData)
 from firebase_admin import remote_config
+from tests import testutils
 
 VERSION_INFO = {
     'versionNumber': '86',
@@ -63,25 +63,17 @@
     }
 
 class TestEvaluate:
-    def set_up(self):
-        # Create a more specific mock for firebase_admin.App
-        self.mock_app = mock.create_autospec(firebase_admin.App)
-        self.mock_app.project_id = 'mock-project-id'
-        self.mock_app.name = 'mock-app-name'
+    @classmethod
+    def setup_class(cls):
+        cred = testutils.MockCredential()
+        firebase_admin.initialize_app(cred, {'projectId': 'project-id'})
 
-        # Mock initialize_app to return the mock App instance
-        self.mock_initialize_app = mock.patch('firebase_admin.initialize_app').start()
-        self.mock_initialize_app.return_value = self.mock_app
-
-        # Mock the app registry
-        self.mock_get_app = mock.patch('firebase_admin._utils.get_app_service').start()
-        self.mock_get_app.return_value = self.mock_app
-
-    def tear_down(self):
-        mock.patch.stopall()
+    @classmethod
+    def teardown_class(cls):
+        testutils.cleanup_apps()
 
     def test_evaluate_or_and_true_condition_true(self):
-        self.set_up()
+        app = firebase_admin.get_app()
         default_config = {'param1': 'in_app_default_param1', 'param3': 'in_app_default_param3'}
         condition = {
             'name': 'is_true',
@@ -116,17 +108,16 @@ def test_evaluate_or_and_true_condition_true(self):
             'etag': '123'
         }
         server_template = remote_config.init_server_template(
-            app=self.mock_app,
+            app=app,
             default_config=default_config,
             template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
         )
 
         server_config = server_template.evaluate()
         assert server_config.get_boolean('is_enabled')
-        self.tear_down()
 
     def test_evaluate_or_and_false_condition_false(self):
-        self.set_up()
+        app = firebase_admin.get_app()
         default_config = {'param1': 'in_app_default_param1', 'param3': 'in_app_default_param3'}
         condition = {
             'name': 'is_true',
@@ -161,17 +152,16 @@ def test_evaluate_or_and_false_condition_false(self):
             'etag': '123'
         }
         server_template = remote_config.init_server_template(
-            app=self.mock_app,
+            app=app,
             default_config=default_config,
             template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
         )
 
         server_config = server_template.evaluate()
         assert not server_config.get_boolean('is_enabled')
-        self.tear_down()
 
     def test_evaluate_non_or_condition(self):
-        self.set_up()
+        app = firebase_admin.get_app()
         default_config = {'param1': 'in_app_default_param1', 'param3': 'in_app_default_param3'}
         condition = {
             'name': 'is_true',
@@ -193,17 +183,16 @@ def test_evaluate_non_or_condition(self):
             'etag': '123'
         }
         server_template = remote_config.init_server_template(
-            app=self.mock_app,
+            app=app,
             default_config=default_config,
             template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
         )
 
         server_config = server_template.evaluate()
         assert server_config.get_boolean('is_enabled')
-        self.tear_down()
 
     def test_evaluate_return_conditional_values_honor_order(self):
-        self.set_up()
+        app = firebase_admin.get_app()
         default_config = {'param1': 'in_app_default_param1', 'param3': 'in_app_default_param3'}
         template_data = {
             'conditions': [
@@ -260,47 +249,44 @@ def test_evaluate_return_conditional_values_honor_order(self):
             'etag': '123'
         }
         server_template = remote_config.init_server_template(
-            app=self.mock_app,
+            app=app,
             default_config=default_config,
             template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
         )
         server_config = server_template.evaluate()
         assert server_config.get_string('dog_type') == 'corgi'
-        self.tear_down()
 
     def test_evaluate_default_when_no_param(self):
-        self.set_up()
+        app = firebase_admin.get_app()
         default_config = {'promo_enabled': False, 'promo_discount': 20,}
         template_data = SERVER_REMOTE_CONFIG_RESPONSE
         template_data['parameters'] = {}
         server_template = remote_config.init_server_template(
-            app=self.mock_app,
+            app=app,
             default_config=default_config,
             template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
         )
         server_config = server_template.evaluate()
         assert server_config.get_boolean('promo_enabled') == default_config.get('promo_enabled')
         assert server_config.get_int('promo_discount') == default_config.get('promo_discount')
-        self.tear_down()
 
     def test_evaluate_default_when_no_default_value(self):
-        self.set_up()
+        app = firebase_admin.get_app()
         default_config = {'default_value': 'local default'}
         template_data = SERVER_REMOTE_CONFIG_RESPONSE
         template_data['parameters'] = {
             'default_value': {}
         }
         server_template = remote_config.init_server_template(
-            app=self.mock_app,
+            app=app,
             default_config=default_config,
             template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
         )
         server_config = server_template.evaluate()
         assert server_config.get_string('default_value') == default_config.get('default_value')
-        self.tear_down()
 
     def test_evaluate_default_when_in_default(self):
-        self.set_up()
+        app = firebase_admin.get_app()
         template_data = SERVER_REMOTE_CONFIG_RESPONSE
         template_data['parameters'] = {
             'remote_default_value': {}
@@ -309,63 +295,59 @@ def test_evaluate_default_when_in_default(self):
             'inapp_default': '🐕'
         }
         server_template = remote_config.init_server_template(
-            app=self.mock_app,
+            app=app,
             default_config=default_config,
             template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
         )
         server_config = server_template.evaluate()
         assert server_config.get_string('inapp_default') == default_config.get('inapp_default')
-        self.tear_down()
 
     def test_evaluate_default_when_defined(self):
-        self.set_up()
+        app = firebase_admin.get_app()
         template_data = SERVER_REMOTE_CONFIG_RESPONSE
         template_data['parameters'] = {}
         default_config = {
             'dog_type': 'shiba'
         }
         server_template = remote_config.init_server_template(
-            app=self.mock_app,
+            app=app,
             default_config=default_config,
             template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
         )
         server_config = server_template.evaluate()
         assert server_config.get_value('dog_type').as_string() == 'shiba'
         assert server_config.get_value('dog_type').get_source() == 'default'
-        self.tear_down()
 
     def test_evaluate_return_numeric_value(self):
-        self.set_up()
+        app = firebase_admin.get_app()
         template_data = SERVER_REMOTE_CONFIG_RESPONSE
         default_config = {
             'dog_age': 12
         }
         server_template = remote_config.init_server_template(
-            app=self.mock_app,
+            app=app,
             default_config=default_config,
             template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
         )
         server_config = server_template.evaluate()
         assert server_config.get_int('dog_age') == 12
-        self.tear_down()
 
     def test_evaluate_return__value(self):
-        self.set_up()
+        app = firebase_admin.get_app()
         template_data = SERVER_REMOTE_CONFIG_RESPONSE
         default_config = {
             'dog_is_cute': True
         }
         server_template = remote_config.init_server_template(
-            app=self.mock_app,
+            app=app,
             default_config=default_config,
             template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
         )
         server_config = server_template.evaluate()
         assert server_config.get_int('dog_is_cute')
-        self.tear_down()
 
     def test_evaluate_unknown_operator_to_false(self):
-        self.set_up()
+        app = firebase_admin.get_app()
         condition = {
             'name': 'is_true',
             'condition': {
@@ -399,16 +381,15 @@ def test_evaluate_unknown_operator_to_false(self):
         }
         context = {'randomization_id': '123'}
         server_template = remote_config.init_server_template(
-            app=self.mock_app,
+            app=app,
             default_config=default_config,
             template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
         )
         server_config = server_template.evaluate(context)
         assert not server_config.get_boolean('is_enabled')
-        self.tear_down()
 
     def test_evaluate_less_or_equal_to_max_to_true(self):
-        self.set_up()
+        app = firebase_admin.get_app()
         condition = {
             'name': 'is_true',
             'condition': {
@@ -444,16 +425,15 @@ def test_evaluate_less_or_equal_to_max_to_true(self):
         }
         context = {'randomization_id': '123'}
         server_template = remote_config.init_server_template(
-            app=self.mock_app,
+            app=app,
             default_config=default_config,
             template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
         )
         server_config = server_template.evaluate(context)
         assert server_config.get_boolean('is_enabled')
-        self.tear_down()
 
     def test_evaluate_undefined_micropercent_to_false(self):
-        self.set_up()
+        app = firebase_admin.get_app()
         condition = {
             'name': 'is_true',
             'condition': {
@@ -488,16 +468,15 @@ def test_evaluate_undefined_micropercent_to_false(self):
         }
         context = {'randomization_id': '123'}
         server_template = remote_config.init_server_template(
-            app=self.mock_app,
+            app=app,
             default_config=default_config,
             template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
         )
         server_config = server_template.evaluate(context)
         assert not server_config.get_boolean('is_enabled')
-        self.tear_down()
 
     def test_evaluate_undefined_micropercentrange_to_false(self):
-        self.set_up()
+        app = firebase_admin.get_app()
         condition = {
             'name': 'is_true',
             'condition': {
@@ -532,16 +511,15 @@ def test_evaluate_undefined_micropercentrange_to_false(self):
         }
         context = {'randomization_id': '123'}
         server_template = remote_config.init_server_template(
-            app=self.mock_app,
+            app=app,
             default_config=default_config,
             template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
         )
         server_config = server_template.evaluate(context)
         assert not server_config.get_boolean('is_enabled')
-        self.tear_down()
 
     def test_evaluate_between_min_max_to_true(self):
-        self.set_up()
+        app = firebase_admin.get_app()
         condition = {
             'name': 'is_true',
             'condition': {
@@ -580,16 +558,15 @@ def test_evaluate_between_min_max_to_true(self):
         }
         context = {'randomization_id': '123'}
         server_template = remote_config.init_server_template(
-            app=self.mock_app,
+            app=app,
             default_config=default_config,
             template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
         )
         server_config = server_template.evaluate(context)
         assert server_config.get_boolean('is_enabled')
-        self.tear_down()
 
     def test_evaluate_between_equal_bounds_to_false(self):
-        self.set_up()
+        app = firebase_admin.get_app()
         condition = {
             'name': 'is_true',
             'condition': {
@@ -628,16 +605,15 @@ def test_evaluate_between_equal_bounds_to_false(self):
         }
         context = {'randomization_id': '123'}
         server_template = remote_config.init_server_template(
-            app=self.mock_app,
+            app=app,
             default_config=default_config,
             template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
         )
         server_config = server_template.evaluate(context)
         assert not server_config.get_boolean('is_enabled')
-        self.tear_down()
 
     def test_evaluate_less_or_equal_to_approx(self):
-        self.set_up()
+        app = firebase_admin.get_app()
         condition = {
             'name': 'is_true',
             'condition': {
@@ -661,14 +637,13 @@ def test_evaluate_less_or_equal_to_approx(self):
         }
 
         truthy_assignments = self.evaluate_random_assignments(condition, 100000,
-                                                              self.mock_app, default_config)
+                                                              app, default_config)
         tolerance = 284
         assert truthy_assignments >= 10000 - tolerance
         assert truthy_assignments <= 10000 + tolerance
-        self.tear_down()
 
     def test_evaluate_between_approx(self):
-        self.set_up()
+        app = firebase_admin.get_app()
         condition = {
             'name': 'is_true',
             'condition': {
@@ -695,14 +670,13 @@ def test_evaluate_between_approx(self):
         }
 
         truthy_assignments = self.evaluate_random_assignments(condition, 100000,
-                                                              self.mock_app, default_config)
+                                                              app, default_config)
         tolerance = 379
         assert truthy_assignments >= 20000 - tolerance
         assert truthy_assignments <= 20000 + tolerance
-        self.tear_down()
 
     def test_evaluate_between_interquartile_range_accuracy(self):
-        self.set_up()
+        app = firebase_admin.get_app()
         condition = {
             'name': 'is_true',
             'condition': {
@@ -729,11 +703,10 @@ def test_evaluate_between_interquartile_range_accuracy(self):
         }
 
         truthy_assignments = self.evaluate_random_assignments(condition, 100000,
-                                                              self.mock_app, default_config)
+                                                              app, default_config)
         tolerance = 474
         assert truthy_assignments >= 50000 - tolerance
         assert truthy_assignments <= 50000 + tolerance
-        self.tear_down()
 
     def evaluate_random_assignments(self, condition, num_of_assignments, mock_app, default_config):
         """Evaluates random assignments based on a condition.

From c736126bb4c688bd0b3b195fde91c6e36f809230 Mon Sep 17 00:00:00 2001
From: Varun Rathore <varunrathore@google.com>
Date: Fri, 15 Nov 2024 13:57:22 +0530
Subject: [PATCH 14/15] Added fix for comments

---
 tests/testutils.py | 6 ------
 1 file changed, 6 deletions(-)

diff --git a/tests/testutils.py b/tests/testutils.py
index 3893a3463..17013b469 100644
--- a/tests/testutils.py
+++ b/tests/testutils.py
@@ -223,7 +223,6 @@ def build_mock_condition(name, condition):
     return {
         'name': name,
         'condition': condition,
-        # ... other relevant fields ...
     }
 
 def build_mock_parameter(name, description, value=None,
@@ -235,20 +234,17 @@ def build_mock_parameter(name, description, value=None,
         'conditionalValues': conditional_values,
         'defaultValue': default_value,
         'parameterGroups': parameter_groups,
-        # ... other relevant fields ...
     }
 
 def build_mock_conditional_value(condition_name, value):
     return {
         'conditionName': condition_name,
         'value': value,
-        # ... other relevant fields ...
     }
 
 def build_mock_default_value(value):
     return {
         'value': value,
-        # ... other relevant fields ...
     }
 
 def build_mock_parameter_group(name, description, parameters):
@@ -256,11 +252,9 @@ def build_mock_parameter_group(name, description, parameters):
         'name': name,
         'description': description,
         'parameters': parameters,
-        # ... other relevant fields ...
     }
 
 def build_mock_version(version_number):
     return {
         'versionNumber': version_number,
-        # ... other relevant fields ...
     }

From 1c27d548e4496aa072120648637615ed4b3229a8 Mon Sep 17 00:00:00 2001
From: Varun Rathore <varunrathore@google.com>
Date: Fri, 15 Nov 2024 18:56:26 +0530
Subject: [PATCH 15/15] Added fix for comments

---
 firebase_admin/remote_config.py | 29 +++++++++++++++++--------
 tests/test_remote_config.py     | 38 ++++++++++++++++-----------------
 2 files changed, 39 insertions(+), 28 deletions(-)

diff --git a/firebase_admin/remote_config.py b/firebase_admin/remote_config.py
index 0d92bbc52..6cc63b94b 100644
--- a/firebase_admin/remote_config.py
+++ b/firebase_admin/remote_config.py
@@ -169,10 +169,15 @@ def get_string(self, key):
         return self.get_value(key).as_string()
 
     def get_int(self, key):
-        return self.get_value(key).as_number()
+        return self.get_value(key).as_int()
+
+    def get_float(self, key):
+        return self.get_value(key).as_float()
 
     def get_value(self, key):
-        return self._config_values[key]
+        if self._config_values[key]:
+            return self._config_values[key]
+        return _Value('static')
 
 
 class _RemoteConfigService:
@@ -622,7 +627,8 @@ class _Value:
     """
     DEFAULT_VALUE_FOR_BOOLEAN = False
     DEFAULT_VALUE_FOR_STRING = ''
-    DEFAULT_VALUE_FOR_NUMBER = 0
+    DEFAULT_VALUE_FOR_INTEGER = 0
+    DEFAULT_VALUE_FOR_FLOAT_NUMBER = 0.0
     BOOLEAN_TRUTHY_VALUES = ['1', 'true', 't', 'yes', 'y', 'on']
 
     def __init__(self, source: ValueSource, value: str = DEFAULT_VALUE_FOR_STRING):
@@ -637,6 +643,8 @@ def __init__(self, source: ValueSource, value: str = DEFAULT_VALUE_FOR_STRING):
 
     def as_string(self) -> str:
         """Returns the value as a string."""
+        if self.source == 'static':
+            return self.DEFAULT_VALUE_FOR_STRING
         return self.value
 
     def as_boolean(self) -> bool:
@@ -645,14 +653,17 @@ def as_boolean(self) -> bool:
             return self.DEFAULT_VALUE_FOR_BOOLEAN
         return str(self.value).lower() in self.BOOLEAN_TRUTHY_VALUES
 
-    def as_number(self) -> float:
+    def as_int(self) -> float:
         """Returns the value as a number."""
         if self.source == 'static':
-            return self.DEFAULT_VALUE_FOR_NUMBER
-        try:
-            return float(self.value)
-        except ValueError:
-            return self.DEFAULT_VALUE_FOR_NUMBER
+            return self.DEFAULT_VALUE_FOR_INTEGER
+        return self.value
+
+    def as_float(self) -> float:
+        """Returns the value as a number."""
+        if self.source == 'static':
+            return self.DEFAULT_VALUE_FOR_FLOAT_NUMBER
+        return float(self.value)
 
     def get_source(self) -> ValueSource:
         """Returns the source of the value."""
diff --git a/tests/test_remote_config.py b/tests/test_remote_config.py
index 5cba92ce5..b503ca28f 100644
--- a/tests/test_remote_config.py
+++ b/tests/test_remote_config.py
@@ -110,7 +110,7 @@ def test_evaluate_or_and_true_condition_true(self):
         server_template = remote_config.init_server_template(
             app=app,
             default_config=default_config,
-            template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
+            template_data=ServerTemplateData('etag', template_data)
         )
 
         server_config = server_template.evaluate()
@@ -154,7 +154,7 @@ def test_evaluate_or_and_false_condition_false(self):
         server_template = remote_config.init_server_template(
             app=app,
             default_config=default_config,
-            template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
+            template_data=ServerTemplateData('etag', template_data)
         )
 
         server_config = server_template.evaluate()
@@ -185,7 +185,7 @@ def test_evaluate_non_or_condition(self):
         server_template = remote_config.init_server_template(
             app=app,
             default_config=default_config,
-            template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
+            template_data=ServerTemplateData('etag', template_data)
         )
 
         server_config = server_template.evaluate()
@@ -251,7 +251,7 @@ def test_evaluate_return_conditional_values_honor_order(self):
         server_template = remote_config.init_server_template(
             app=app,
             default_config=default_config,
-            template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
+            template_data=ServerTemplateData('etag', template_data)
         )
         server_config = server_template.evaluate()
         assert server_config.get_string('dog_type') == 'corgi'
@@ -264,7 +264,7 @@ def test_evaluate_default_when_no_param(self):
         server_template = remote_config.init_server_template(
             app=app,
             default_config=default_config,
-            template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
+            template_data=ServerTemplateData('etag', template_data)
         )
         server_config = server_template.evaluate()
         assert server_config.get_boolean('promo_enabled') == default_config.get('promo_enabled')
@@ -280,7 +280,7 @@ def test_evaluate_default_when_no_default_value(self):
         server_template = remote_config.init_server_template(
             app=app,
             default_config=default_config,
-            template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
+            template_data=ServerTemplateData('etag', template_data)
         )
         server_config = server_template.evaluate()
         assert server_config.get_string('default_value') == default_config.get('default_value')
@@ -297,7 +297,7 @@ def test_evaluate_default_when_in_default(self):
         server_template = remote_config.init_server_template(
             app=app,
             default_config=default_config,
-            template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
+            template_data=ServerTemplateData('etag', template_data)
         )
         server_config = server_template.evaluate()
         assert server_config.get_string('inapp_default') == default_config.get('inapp_default')
@@ -312,7 +312,7 @@ def test_evaluate_default_when_defined(self):
         server_template = remote_config.init_server_template(
             app=app,
             default_config=default_config,
-            template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
+            template_data=ServerTemplateData('etag', template_data)
         )
         server_config = server_template.evaluate()
         assert server_config.get_value('dog_type').as_string() == 'shiba'
@@ -327,12 +327,12 @@ def test_evaluate_return_numeric_value(self):
         server_template = remote_config.init_server_template(
             app=app,
             default_config=default_config,
-            template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
+            template_data=ServerTemplateData('etag', template_data)
         )
         server_config = server_template.evaluate()
         assert server_config.get_int('dog_age') == 12
 
-    def test_evaluate_return__value(self):
+    def test_evaluate_return_boolean_value(self):
         app = firebase_admin.get_app()
         template_data = SERVER_REMOTE_CONFIG_RESPONSE
         default_config = {
@@ -341,10 +341,10 @@ def test_evaluate_return__value(self):
         server_template = remote_config.init_server_template(
             app=app,
             default_config=default_config,
-            template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
+            template_data=ServerTemplateData('etag', template_data)
         )
         server_config = server_template.evaluate()
-        assert server_config.get_int('dog_is_cute')
+        assert server_config.get_boolean('dog_is_cute')
 
     def test_evaluate_unknown_operator_to_false(self):
         app = firebase_admin.get_app()
@@ -383,7 +383,7 @@ def test_evaluate_unknown_operator_to_false(self):
         server_template = remote_config.init_server_template(
             app=app,
             default_config=default_config,
-            template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
+            template_data=ServerTemplateData('etag', template_data)
         )
         server_config = server_template.evaluate(context)
         assert not server_config.get_boolean('is_enabled')
@@ -427,7 +427,7 @@ def test_evaluate_less_or_equal_to_max_to_true(self):
         server_template = remote_config.init_server_template(
             app=app,
             default_config=default_config,
-            template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
+            template_data=ServerTemplateData('etag', template_data)
         )
         server_config = server_template.evaluate(context)
         assert server_config.get_boolean('is_enabled')
@@ -470,7 +470,7 @@ def test_evaluate_undefined_micropercent_to_false(self):
         server_template = remote_config.init_server_template(
             app=app,
             default_config=default_config,
-            template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
+            template_data=ServerTemplateData('etag', template_data)
         )
         server_config = server_template.evaluate(context)
         assert not server_config.get_boolean('is_enabled')
@@ -513,7 +513,7 @@ def test_evaluate_undefined_micropercentrange_to_false(self):
         server_template = remote_config.init_server_template(
             app=app,
             default_config=default_config,
-            template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
+            template_data=ServerTemplateData('etag', template_data)
         )
         server_config = server_template.evaluate(context)
         assert not server_config.get_boolean('is_enabled')
@@ -560,7 +560,7 @@ def test_evaluate_between_min_max_to_true(self):
         server_template = remote_config.init_server_template(
             app=app,
             default_config=default_config,
-            template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
+            template_data=ServerTemplateData('etag', template_data)
         )
         server_config = server_template.evaluate(context)
         assert server_config.get_boolean('is_enabled')
@@ -607,7 +607,7 @@ def test_evaluate_between_equal_bounds_to_false(self):
         server_template = remote_config.init_server_template(
             app=app,
             default_config=default_config,
-            template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
+            template_data=ServerTemplateData('etag', template_data)
         )
         server_config = server_template.evaluate(context)
         assert not server_config.get_boolean('is_enabled')
@@ -735,7 +735,7 @@ def evaluate_random_assignments(self, condition, num_of_assignments, mock_app, d
         server_template = remote_config.init_server_template(
             app=mock_app,
             default_config=default_config,
-            template_data=ServerTemplateData('etag', template_data)  # Use ServerTemplateData here
+            template_data=ServerTemplateData('etag', template_data)
         )
 
         for _ in range(num_of_assignments):