From 7072a860172e554873c6bae597af5072392c19e7 Mon Sep 17 00:00:00 2001 From: ubaskota <19787410+ubaskota@users.noreply.github.com> Date: Fri, 27 Feb 2026 23:00:43 -0500 Subject: [PATCH 1/2] Add functional tests for config resolution mechanism --- .../functional/test_config_resolution.py | 182 ++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 packages/smithy-aws-core/tests/functional/test_config_resolution.py diff --git a/packages/smithy-aws-core/tests/functional/test_config_resolution.py b/packages/smithy-aws-core/tests/functional/test_config_resolution.py new file mode 100644 index 00000000..f3109f00 --- /dev/null +++ b/packages/smithy-aws-core/tests/functional/test_config_resolution.py @@ -0,0 +1,182 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +from typing import Any + +import pytest +from pytest import MonkeyPatch +from smithy_aws_core.config.custom_resolvers import resolve_retry_strategy +from smithy_aws_core.config.sources import EnvironmentSource +from smithy_aws_core.config.validators import ( + ConfigValidationError, + validate_max_attempts, + validate_retry_mode, +) +from smithy_core.config.property import ConfigProperty +from smithy_core.config.resolver import ConfigResolver +from smithy_core.interfaces.config import ConfigSource +from smithy_core.retries import RetryStrategyOptions + + +class BaseTestConfig: + """Base config class with common functionality for tests.""" + + _resolver: ConfigResolver + + def __init__(self, sources: list[ConfigSource] | None = None) -> None: + if sources is None: + sources = [EnvironmentSource()] + self._resolver = ConfigResolver(sources=sources) + + def get_source(self, key: str) -> str | None: + cached = self.__dict__.get(f"_cache_{key}") + return cached[1] if cached else None + + +def make_config_class(properties: dict[str, ConfigProperty]) -> type[BaseTestConfig]: + """Factory function to create a config class with specified properties. + + :param properties: Dict mapping property names to ConfigProperty instances + + :returns: A new config class with the specified properties + """ + class_dict: dict[str, Any] = { + "__init__": BaseTestConfig.__init__, + "get_source": BaseTestConfig.get_source, + } + class_dict.update(properties) + + return type("TestConfig", (BaseTestConfig,), class_dict) + + +class TestConfigResolutionEndToEnd: + """Functional tests for complete config resolution flow.""" + + def test_environment_var_resolution(self, monkeypatch: MonkeyPatch) -> None: + TestConfig = make_config_class( + {"region": ConfigProperty("region", default_value="us-east-1")} + ) + monkeypatch.setenv("AWS_REGION", "eu-west-1") + + config = TestConfig() + + assert config.region == "eu-west-1" # type: ignore + assert config.get_source("region") == "environment" + + def test_instance_overrides_environment(self, monkeypatch: MonkeyPatch) -> None: + TestConfig = make_config_class( + {"region": ConfigProperty("region", default_value="us-east-1")} + ) + + config = TestConfig() + config.region = "us-west-2" # type: ignore + + monkeypatch.setenv("AWS_REGION", "eu-west-1") + + assert config.region == "us-west-2" # type: ignore + assert config.get_source("region") == "instance" + + def test_complex_resolution_with_custom_resolver( + self, monkeypatch: MonkeyPatch + ) -> None: + TestConfig = make_config_class( + { + "retry_strategy": ConfigProperty( + "retry_strategy", + resolver_func=resolve_retry_strategy, + default_value=RetryStrategyOptions( + retry_mode="standard", max_attempts=3 + ), + ) + } + ) + + monkeypatch.setenv("AWS_RETRY_MODE", "standard") + monkeypatch.setenv("AWS_MAX_ATTEMPTS", "5") + + config = TestConfig() + + retry_strategy = config.retry_strategy # type: ignore + assert isinstance(retry_strategy, RetryStrategyOptions) + assert retry_strategy.retry_mode == "standard" + assert retry_strategy.max_attempts == 5 + + source = config.get_source("retry_strategy") + assert source == "retry_mode=environment, max_attempts=environment" + + def test_caching_behavior(self, monkeypatch: MonkeyPatch) -> None: + TestConfig = make_config_class( + {"region": ConfigProperty("region", default_value="us-east-1")} + ) + + monkeypatch.setenv("AWS_REGION", "ap-south-1") + + config = TestConfig() + + region1 = config.region # type: ignore + + monkeypatch.setenv("AWS_REGION", "eu-central-1") + + region2 = config.region # type: ignore + # The first value for region which is cached is returned + assert region1 == region2 == "ap-south-1" + + def test_validation_error_during_resolution(self, monkeypatch: MonkeyPatch) -> None: + TestConfig = make_config_class( + { + "max_attempts": ConfigProperty( + "max_attempts", validator=validate_max_attempts, default_value=3 + ) + } + ) + + monkeypatch.setenv("AWS_MAX_ATTEMPTS", "invalid") + + config = TestConfig() + + with pytest.raises( + ConfigValidationError, match="max_attempts must be a number" + ): + config.max_attempts # type: ignore + + def test_returns_default_region_when_source_empty(self) -> None: + TestConfig = make_config_class( + { + "region": ConfigProperty("region", default_value="us-east-1"), + "max_attempts": ConfigProperty("max_attempts", default_value=3), + } + ) + + config = TestConfig(sources=[]) + + assert config.region == "us-east-1" # type: ignore + assert config.max_attempts == 3 # type: ignore + + def test_returns_default_retry_mode_when_source_empty(self) -> None: + TestConfig = make_config_class( + { + "retry_mode": ConfigProperty( + "retry_mode", + validator=validate_retry_mode, + default_value="standard", + ) + } + ) + + config = TestConfig(sources=[]) + assert config.retry_mode == "standard" # type: ignore + + def test_validation_error_when_value_assigned(self) -> None: + TestConfig = make_config_class( + { + "retry_mode": ConfigProperty( + "retry_mode", + validator=validate_retry_mode, + default_value="standard", + ) + } + ) + config = TestConfig(sources=[]) + + with pytest.raises(ConfigValidationError, match="retry_mode must be one of"): + config.retry_mode = "invalid_mode" # type: ignore From 03097a125041c04eb44e5addf43566c74b1ab614 Mon Sep 17 00:00:00 2001 From: ubaskota <19787410+ubaskota@users.noreply.github.com> Date: Thu, 5 Mar 2026 14:27:56 -0500 Subject: [PATCH 2/2] Update functional and unit tests --- .../functional/test_config_resolution.py | 129 +++++++++++++----- .../tests/unit/config/test_resolver.py | 26 ++++ 2 files changed, 119 insertions(+), 36 deletions(-) diff --git a/packages/smithy-aws-core/tests/functional/test_config_resolution.py b/packages/smithy-aws-core/tests/functional/test_config_resolution.py index f3109f00..47ae7a97 100644 --- a/packages/smithy-aws-core/tests/functional/test_config_resolution.py +++ b/packages/smithy-aws-core/tests/functional/test_config_resolution.py @@ -10,6 +10,7 @@ from smithy_aws_core.config.validators import ( ConfigValidationError, validate_max_attempts, + validate_region, validate_retry_mode, ) from smithy_core.config.property import ConfigProperty @@ -33,7 +34,7 @@ def get_source(self, key: str) -> str | None: return cached[1] if cached else None -def make_config_class(properties: dict[str, ConfigProperty]) -> type[BaseTestConfig]: +def make_config_class(properties: dict[str, ConfigProperty]) -> Any: """Factory function to create a config class with specified properties. :param properties: Dict mapping property names to ConfigProperty instances @@ -49,7 +50,7 @@ def make_config_class(properties: dict[str, ConfigProperty]) -> type[BaseTestCon return type("TestConfig", (BaseTestConfig,), class_dict) -class TestConfigResolutionEndToEnd: +class TestConfigResolution: """Functional tests for complete config resolution flow.""" def test_environment_var_resolution(self, monkeypatch: MonkeyPatch) -> None: @@ -60,7 +61,7 @@ def test_environment_var_resolution(self, monkeypatch: MonkeyPatch) -> None: config = TestConfig() - assert config.region == "eu-west-1" # type: ignore + assert config.region == "eu-west-1" assert config.get_source("region") == "environment" def test_instance_overrides_environment(self, monkeypatch: MonkeyPatch) -> None: @@ -69,11 +70,11 @@ def test_instance_overrides_environment(self, monkeypatch: MonkeyPatch) -> None: ) config = TestConfig() - config.region = "us-west-2" # type: ignore + config.region = "us-west-2" monkeypatch.setenv("AWS_REGION", "eu-west-1") - assert config.region == "us-west-2" # type: ignore + assert config.region == "us-west-2" assert config.get_source("region") == "instance" def test_complex_resolution_with_custom_resolver( @@ -84,9 +85,7 @@ def test_complex_resolution_with_custom_resolver( "retry_strategy": ConfigProperty( "retry_strategy", resolver_func=resolve_retry_strategy, - default_value=RetryStrategyOptions( - retry_mode="standard", max_attempts=3 - ), + default_value=RetryStrategyOptions(retry_mode="standard"), ) } ) @@ -96,7 +95,7 @@ def test_complex_resolution_with_custom_resolver( config = TestConfig() - retry_strategy = config.retry_strategy # type: ignore + retry_strategy = config.retry_strategy assert isinstance(retry_strategy, RetryStrategyOptions) assert retry_strategy.retry_mode == "standard" assert retry_strategy.max_attempts == 5 @@ -113,46 +112,91 @@ def test_caching_behavior(self, monkeypatch: MonkeyPatch) -> None: config = TestConfig() - region1 = config.region # type: ignore + region1 = config.region monkeypatch.setenv("AWS_REGION", "eu-central-1") - region2 = config.region # type: ignore + region2 = config.region # The first value for region which is cached is returned assert region1 == region2 == "ap-south-1" - def test_validation_error_during_resolution(self, monkeypatch: MonkeyPatch) -> None: + @pytest.mark.parametrize( + "property_name,property_config,expected_value", + [ + ( + "region", + ConfigProperty("region", default_value="us-east-1"), + "us-east-1", + ), + ( + "retry_mode", + ConfigProperty( + "retry_mode", + validator=validate_retry_mode, + default_value="standard", + ), + "standard", + ), + ], + ) + def test_uses_default_when_no_sources( + self, property_name: str, property_config: ConfigProperty, expected_value: str + ) -> None: + TestConfig = make_config_class({property_name: property_config}) + config = TestConfig(sources=[]) + + assert getattr(config, property_name) == expected_value + assert config.get_source(property_name) == "default" + + def test_default_value_for_complex_resolution( + self, monkeypatch: MonkeyPatch + ) -> None: TestConfig = make_config_class( { - "max_attempts": ConfigProperty( - "max_attempts", validator=validate_max_attempts, default_value=3 + "retry_strategy": ConfigProperty( + "retry_strategy", + resolver_func=resolve_retry_strategy, + default_value=RetryStrategyOptions(retry_mode="standard"), ) } ) - monkeypatch.setenv("AWS_MAX_ATTEMPTS", "invalid") - config = TestConfig() - with pytest.raises( - ConfigValidationError, match="max_attempts must be a number" - ): - config.max_attempts # type: ignore + retry_strategy = config.retry_strategy + assert isinstance(retry_strategy, RetryStrategyOptions) + assert retry_strategy.retry_mode == "standard" + # None for max_attempts means the RetryStrategy will use its + # own default max_attempts value for the set retry_mode + assert retry_strategy.max_attempts is None + source = config.get_source("retry_strategy") + assert source == "default" - def test_returns_default_region_when_source_empty(self) -> None: + def test_retry_strategy_combines_multiple_sources( + self, monkeypatch: MonkeyPatch + ) -> None: TestConfig = make_config_class( { - "region": ConfigProperty("region", default_value="us-east-1"), - "max_attempts": ConfigProperty("max_attempts", default_value=3), + "retry_strategy": ConfigProperty( + "retry_strategy", + resolver_func=resolve_retry_strategy, + default_value=RetryStrategyOptions(retry_mode="standard"), + ) } ) - config = TestConfig(sources=[]) + monkeypatch.setenv("AWS_MAX_ATTEMPTS", "10") + config = TestConfig() - assert config.region == "us-east-1" # type: ignore - assert config.max_attempts == 3 # type: ignore + retry_strategy = config.retry_strategy + assert retry_strategy.retry_mode == "standard" + assert retry_strategy.max_attempts == 10 + assert ( + config.get_source("retry_strategy") + == "retry_mode=default, max_attempts=environment" + ) - def test_returns_default_retry_mode_when_source_empty(self) -> None: + def test_validation_error_when_value_assigned(self) -> None: TestConfig = make_config_class( { "retry_mode": ConfigProperty( @@ -162,21 +206,34 @@ def test_returns_default_retry_mode_when_source_empty(self) -> None: ) } ) - config = TestConfig(sources=[]) - assert config.retry_mode == "standard" # type: ignore - def test_validation_error_when_value_assigned(self) -> None: + with pytest.raises(ConfigValidationError, match="retry_mode must be one of"): + config.retry_mode = "invalid_mode" + + def test_validation_error_during_resolution(self, monkeypatch: MonkeyPatch) -> None: TestConfig = make_config_class( { - "retry_mode": ConfigProperty( - "retry_mode", - validator=validate_retry_mode, - default_value="standard", + "max_attempts": ConfigProperty( + "max_attempts", validator=validate_max_attempts, default_value=3 ) } ) + + monkeypatch.setenv("AWS_MAX_ATTEMPTS", "invalid") + + config = TestConfig() + + with pytest.raises( + ConfigValidationError, match="max_attempts must be a number" + ): + config.max_attempts + + def test_region_validation_fails_when_none(self) -> None: + TestConfig = make_config_class( + {"region": ConfigProperty("region", validator=validate_region)} + ) config = TestConfig(sources=[]) - with pytest.raises(ConfigValidationError, match="retry_mode must be one of"): - config.retry_mode = "invalid_mode" # type: ignore + with pytest.raises(ConfigValidationError, match="region not found"): + config.region diff --git a/packages/smithy-core/tests/unit/config/test_resolver.py b/packages/smithy-core/tests/unit/config/test_resolver.py index bf016e1d..05e921da 100644 --- a/packages/smithy-core/tests/unit/config/test_resolver.py +++ b/packages/smithy-core/tests/unit/config/test_resolver.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 from typing import Any +import pytest from smithy_core.config.resolver import ConfigResolver @@ -110,3 +111,28 @@ def test_treats_empty_string_as_valid_value(self): assert value == "" assert source_name == "test" + + +class TestInvalidSourceHandling: + """Tests for handling invalid or malformed config sources.""" + + def test_invalid_source_missing_get_method(self): + class InvalidSource: + name = "invalid" + # Missing get() method + + resolver = ConfigResolver(sources=[InvalidSource()]) # type: ignore + + with pytest.raises(AttributeError): + resolver.get("region") + + def test_invalid_source_missing_name_property(self): + class InvalidSource: + # Missing source name property + def get(self, key: str): + return "value" + + resolver = ConfigResolver(sources=[InvalidSource()]) # type: ignore + + with pytest.raises(AttributeError): + resolver.get("region")