Skip to content

Commit ef6d5c0

Browse files
committed
Improve test coverage, more e2e tests
1 parent 088a8c6 commit ef6d5c0

File tree

1 file changed

+369
-0
lines changed

1 file changed

+369
-0
lines changed

tests/test_envconfig.py

Lines changed: 369 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,3 +612,372 @@ def test_client_config_to_from_dict():
612612
assert empty_config_dict == {}
613613
new_empty_config = ClientConfig.from_dict(empty_config_dict)
614614
assert empty_config == new_empty_config
615+
616+
617+
def test_grpc_metadata_normalization_from_toml():
618+
"""Test that gRPC metadata keys get normalized from TOML."""
619+
toml_config = textwrap.dedent(
620+
"""
621+
[profile.default]
622+
address = "localhost:7233"
623+
namespace = "default"
624+
625+
[profile.default.grpc_meta]
626+
"Custom-Header" = "custom-value"
627+
"ANOTHER_HEADER_KEY" = "another-value"
628+
"mixed_Case-header" = "mixed-value"
629+
"""
630+
)
631+
632+
profile = ClientConfigProfile.load(config_source=toml_config)
633+
634+
# Keys should be normalized: uppercase -> lowercase, underscores -> hyphens
635+
assert profile.grpc_meta["custom-header"] == "custom-value"
636+
assert profile.grpc_meta["another-header-key"] == "another-value"
637+
assert profile.grpc_meta["mixed-case-header"] == "mixed-value"
638+
639+
# Original case variations should not exist
640+
assert "Custom-Header" not in profile.grpc_meta
641+
assert "ANOTHER_HEADER_KEY" not in profile.grpc_meta
642+
assert "mixed_Case-header" not in profile.grpc_meta
643+
644+
config = profile.to_client_connect_config()
645+
rpc_metadata = config.get("rpc_metadata")
646+
assert rpc_metadata is not None
647+
assert rpc_metadata["custom-header"] == "custom-value"
648+
assert rpc_metadata["another-header-key"] == "another-value"
649+
650+
651+
def test_grpc_metadata_deletion_via_empty_env_value(base_config_file: Path):
652+
"""Test that empty environment variable values delete existing gRPC metadata."""
653+
env = {
654+
# Empty value should remove the header
655+
"TEMPORAL_GRPC_META_CUSTOM_HEADER": "",
656+
# Non-empty value should set the header
657+
"TEMPORAL_GRPC_META_NEW_HEADER": "new-value",
658+
}
659+
profile = ClientConfigProfile.load(
660+
config_source=base_config_file, profile="custom", override_env_vars=env
661+
)
662+
663+
# custom-header should be removed by empty env value
664+
assert "custom-header" not in profile.grpc_meta
665+
# new-header should be added
666+
assert profile.grpc_meta["new-header"] == "new-value"
667+
668+
config = profile.to_client_connect_config()
669+
rpc_metadata = config.get("rpc_metadata")
670+
if rpc_metadata:
671+
assert "custom-header" not in rpc_metadata
672+
assert rpc_metadata["new-header"] == "new-value"
673+
674+
675+
def test_default_profile_not_found_returns_empty_profile():
676+
"""Test that requesting missing 'default' profile returns empty profile instead of error."""
677+
toml_config = textwrap.dedent(
678+
"""
679+
[profile.existing]
680+
address = "my-address"
681+
"""
682+
)
683+
profile = ClientConfigProfile.load(config_source=toml_config)
684+
assert profile.address is None
685+
assert profile.namespace is None
686+
assert profile.api_key is None
687+
assert not profile.grpc_meta
688+
assert profile.tls is None
689+
690+
691+
def test_tls_conflict_across_sources_path_in_toml_data_in_env():
692+
"""Test error when cert path in TOML conflicts with cert data in env var."""
693+
toml_config = textwrap.dedent(
694+
"""
695+
[profile.default]
696+
address = "localhost:7233"
697+
[profile.default.tls]
698+
client_cert_path = "/path/to/cert"
699+
"""
700+
)
701+
702+
env = {
703+
"TEMPORAL_TLS_CLIENT_CERT_DATA": "cert-data-from-env"
704+
}
705+
706+
with pytest.raises(RuntimeError, match="Cannot specify cert data via TEMPORAL_TLS_CLIENT_CERT_DATA when cert path is already specified"):
707+
ClientConfigProfile.load(
708+
config_source=toml_config,
709+
override_env_vars=env
710+
)
711+
712+
713+
def test_tls_conflict_across_sources_data_in_toml_path_in_env():
714+
"""Test error when cert data in TOML conflicts with cert path in env var."""
715+
toml_config = textwrap.dedent(
716+
"""
717+
[profile.default]
718+
address = "localhost:7233"
719+
[profile.default.tls]
720+
client_cert_data = "cert-data-from-toml"
721+
"""
722+
)
723+
724+
env = {
725+
"TEMPORAL_TLS_CLIENT_CERT_PATH": "/path/from/env"
726+
}
727+
728+
with pytest.raises(RuntimeError, match="Cannot specify cert path via TEMPORAL_TLS_CLIENT_CERT_PATH when cert data is already specified"):
729+
ClientConfigProfile.load(
730+
config_source=toml_config,
731+
override_env_vars=env
732+
)
733+
734+
735+
def test_load_client_connect_options_convenience_api(base_config_file: Path):
736+
"""Test the convenience API for loading client connect configuration."""
737+
# Test default profile with file
738+
config = ClientConfig.load_client_connect_config(config_file=str(base_config_file))
739+
assert config.get("target_host") == "default-address"
740+
assert config.get("namespace") == "default-namespace"
741+
742+
# Test with environment overrides
743+
env = {"TEMPORAL_NAMESPACE": "env-override-namespace"}
744+
config_with_env = ClientConfig.load_client_connect_config(
745+
config_file=str(base_config_file), override_env_vars=env
746+
)
747+
assert config_with_env.get("target_host") == "default-address"
748+
assert config_with_env.get("namespace") == "env-override-namespace"
749+
750+
# Test with specific profile
751+
config_custom = ClientConfig.load_client_connect_config(
752+
profile="custom", config_file=str(base_config_file)
753+
)
754+
assert config_custom.get("target_host") == "custom-address"
755+
assert config_custom.get("namespace") == "custom-namespace"
756+
assert config_custom.get("api_key") == "custom-api-key"
757+
758+
759+
def test_load_client_connect_options_e2e_validation():
760+
"""Test comprehensive end-to-end configuration loading with all features."""
761+
toml_content = textwrap.dedent(
762+
"""
763+
[profile.production]
764+
address = "prod.temporal.com:443"
765+
namespace = "production-ns"
766+
api_key = "prod-api-key"
767+
768+
[profile.production.tls]
769+
server_name = "prod.temporal.com"
770+
server_ca_cert_data = "prod-ca-cert"
771+
772+
[profile.production.grpc_meta]
773+
authorization = "Bearer prod-token"
774+
"x-custom-header" = "prod-value"
775+
"""
776+
)
777+
778+
env_overrides = {
779+
"TEMPORAL_GRPC_META_X_ENVIRONMENT": "production",
780+
"TEMPORAL_TLS_SERVER_NAME": "override.temporal.com"
781+
}
782+
783+
config = ClientConfig.load_client_connect_config(
784+
profile="production",
785+
config_file=None, # Use config_source directly
786+
override_env_vars=env_overrides,
787+
disable_file=True # Load from config_source instead
788+
)
789+
790+
# First load the profile to get the raw config, then convert
791+
profile = ClientConfigProfile.load(
792+
profile="production",
793+
config_source=toml_content,
794+
override_env_vars=env_overrides
795+
)
796+
config = profile.to_client_connect_config()
797+
798+
# Validate all configuration aspects
799+
assert config.get("target_host") == "prod.temporal.com:443"
800+
assert config.get("namespace") == "production-ns"
801+
assert config.get("api_key") == "prod-api-key"
802+
803+
# TLS configuration (API key should auto-enable TLS)
804+
assert config.get("tls") is not None
805+
tls_config = config.get("tls")
806+
assert isinstance(tls_config, TLSConfig)
807+
assert tls_config.domain == "override.temporal.com" # Env override
808+
assert tls_config.server_root_ca_cert == b"prod-ca-cert"
809+
810+
# gRPC metadata with normalization and env overrides
811+
assert config.get("rpc_metadata") is not None
812+
rpc_metadata = config.get("rpc_metadata")
813+
assert rpc_metadata is not None
814+
assert rpc_metadata["authorization"] == "Bearer prod-token"
815+
assert rpc_metadata["x-custom-header"] == "prod-value"
816+
assert rpc_metadata["x-environment"] == "production" # From env
817+
818+
819+
async def test_e2e_basic_development_profile_client_connection(client: Client):
820+
"""Test basic development profile with actual client connection."""
821+
# Get connection details from the fixture client
822+
target_host = client.service_client.config.target_host
823+
namespace = client.namespace
824+
825+
toml_content = textwrap.dedent(
826+
f"""
827+
[profile.development]
828+
address = "{target_host}"
829+
namespace = "{namespace}"
830+
831+
[profile.development.grpc_meta]
832+
"x-test-source" = "envconfig-python-dev"
833+
"""
834+
)
835+
836+
profile = ClientConfigProfile.load(
837+
profile="development",
838+
config_source=toml_content
839+
)
840+
841+
config = profile.to_client_connect_config()
842+
843+
# Create actual Temporal client using envconfig
844+
new_client = await Client.connect(**config)
845+
846+
# Verify client configuration matches envconfig
847+
assert new_client.service_client.config.target_host == target_host
848+
assert new_client.namespace == namespace
849+
if new_client.service_client.config.rpc_metadata:
850+
assert new_client.service_client.config.rpc_metadata["x-test-source"] == "envconfig-python-dev"
851+
852+
853+
async def test_e2e_production_tls_api_key_client_connection(client: Client):
854+
"""Test production profile with TLS and API key with actual client connection."""
855+
# Get connection details from the fixture client
856+
target_host = client.service_client.config.target_host
857+
858+
toml_content = textwrap.dedent(
859+
f"""
860+
[profile.production]
861+
address = "{target_host}"
862+
namespace = "production-namespace"
863+
api_key = "prod-api-key-123"
864+
865+
[profile.production.tls]
866+
disabled = true
867+
868+
[profile.production.grpc_meta]
869+
authorization = "Bearer prod-token"
870+
"x-environment" = "production"
871+
"""
872+
)
873+
874+
profile = ClientConfigProfile.load(
875+
profile="production",
876+
config_source=toml_content
877+
)
878+
879+
config = profile.to_client_connect_config()
880+
881+
# Create TLS-enabled client with API key
882+
new_client = await Client.connect(**config)
883+
884+
# Verify production configuration
885+
assert new_client.service_client.config.target_host == target_host
886+
assert new_client.namespace == "production-namespace"
887+
assert new_client.service_client.config.api_key == "prod-api-key-123"
888+
if new_client.service_client.config.rpc_metadata:
889+
assert new_client.service_client.config.rpc_metadata["authorization"] == "Bearer prod-token"
890+
assert new_client.service_client.config.rpc_metadata["x-environment"] == "production"
891+
892+
893+
async def test_e2e_environment_overrides_client_connection(client: Client):
894+
"""Test environment overrides with actual client connection."""
895+
# Get connection details from the fixture client
896+
target_host = client.service_client.config.target_host
897+
898+
toml_content = textwrap.dedent(
899+
"""
900+
[profile.staging]
901+
address = "staging.temporal.com:443"
902+
namespace = "staging-namespace"
903+
904+
[profile.staging.grpc_meta]
905+
"x-deployment" = "staging"
906+
authorization = "Bearer staging-token"
907+
"""
908+
)
909+
910+
env_overrides = {
911+
"TEMPORAL_ADDRESS": target_host,
912+
"TEMPORAL_NAMESPACE": "override-namespace",
913+
"TEMPORAL_GRPC_META_X_DEPLOYMENT": "canary",
914+
"TEMPORAL_GRPC_META_AUTHORIZATION": "Bearer override-token"
915+
}
916+
917+
profile = ClientConfigProfile.load(
918+
profile="staging",
919+
config_source=toml_content,
920+
override_env_vars=env_overrides
921+
)
922+
923+
config = profile.to_client_connect_config()
924+
925+
# Create client with environment overrides
926+
new_client = await Client.connect(**config)
927+
928+
# Verify environment overrides took effect
929+
assert new_client.service_client.config.target_host == target_host
930+
assert new_client.namespace == "override-namespace"
931+
if new_client.service_client.config.rpc_metadata:
932+
assert new_client.service_client.config.rpc_metadata["x-deployment"] == "canary"
933+
assert new_client.service_client.config.rpc_metadata["authorization"] == "Bearer override-token"
934+
935+
936+
async def test_e2e_multi_profile_different_client_connections(client: Client):
937+
"""Test multiple profiles creating different client connections."""
938+
# Get connection details from the fixture client
939+
target_host = client.service_client.config.target_host
940+
941+
toml_content = textwrap.dedent(
942+
f"""
943+
[profile.development]
944+
address = "{target_host}"
945+
namespace = "dev"
946+
947+
[profile.production]
948+
address = "{target_host}"
949+
namespace = "prod"
950+
api_key = "prod-key"
951+
952+
[profile.production.tls]
953+
disabled = true
954+
"""
955+
)
956+
957+
# Load and create development client
958+
dev_profile = ClientConfigProfile.load(
959+
profile="development",
960+
config_source=toml_content
961+
)
962+
963+
dev_config = dev_profile.to_client_connect_config()
964+
dev_client = await Client.connect(**dev_config)
965+
966+
# Load and create production client
967+
prod_profile = ClientConfigProfile.load(
968+
profile="production",
969+
config_source=toml_content
970+
)
971+
972+
prod_config = prod_profile.to_client_connect_config()
973+
prod_client = await Client.connect(**prod_config)
974+
975+
# Verify different configurations for each client
976+
assert dev_client.service_client.config.target_host == target_host
977+
assert dev_client.namespace == "dev"
978+
assert dev_client.service_client.config.api_key is None
979+
assert dev_client.service_client.config.tls is False
980+
981+
assert prod_client.service_client.config.target_host == target_host
982+
assert prod_client.namespace == "prod"
983+
assert prod_client.service_client.config.api_key == "prod-key"

0 commit comments

Comments
 (0)