Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/fixed/persistent-pelican.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Keep an empty TLS table when `ClientConfigProfile` and `ClientConfig` are converted to and from dictionaries, so a profile that enables TLS with default settings does not lose it.
12 changes: 8 additions & 4 deletions temporalio/envconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,12 @@ def to_connect_tls_config(self) -> bool | temporalio.service.TLSConfig:

@classmethod
def from_dict(cls, d: ClientConfigTLSDict | None) -> Self | None:
"""Create a ClientConfigTLS from a dictionary."""
if not d:
"""Create a ClientConfigTLS from a dictionary.

``None`` means there is no TLS table. An empty dictionary is a TLS table
with nothing set, so it gives a default ``ClientConfigTLS``.
"""
if d is None:
return None
return cls(
disabled=d.get("disabled"),
Expand Down Expand Up @@ -213,8 +217,8 @@ def to_dict(self) -> ClientConfigProfileDict:
d["namespace"] = self.namespace
if self.api_key is not None:
d["api_key"] = self.api_key
if self.tls and (tls_dict := self.tls.to_dict()):
d["tls"] = tls_dict
if self.tls is not None:
d["tls"] = self.tls.to_dict()
if self.grpc_meta:
d["grpc_meta"] = self.grpc_meta
return d
Expand Down
55 changes: 55 additions & 0 deletions tests/test_envconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import textwrap
from pathlib import Path
from types import SimpleNamespace
from typing import Any
from unittest.mock import AsyncMock

import pytest
Expand Down Expand Up @@ -694,6 +695,60 @@ def test_client_config_to_from_dict():
assert empty_config == new_empty_config


def test_client_config_tls_empty_dict_is_not_missing():
"""Test that an empty TLS table is distinct from no TLS table in both directions."""
empty_tls_profile = ClientConfigProfile(tls=ClientConfigTLS())
assert empty_tls_profile.to_dict() == {"tls": {}}
assert ClientConfigProfile.from_dict({"tls": {}}) == empty_tls_profile
assert ClientConfigTLS.from_dict({}) == ClientConfigTLS()
assert ClientConfigTLS.from_dict(None) is None


@pytest.mark.parametrize(
("toml_tls", "tls_dict", "connect_tls"),
[
pytest.param("", None, None, id="no-tls-table"),
pytest.param("[profile.default.tls]", {}, TLSConfig(), id="empty-tls-table"),
pytest.param(
"[profile.default.tls]\ndisabled = false",
{"disabled": False},
TLSConfig(),
id="disabled-false",
),
pytest.param(
"[profile.default.tls]\ndisabled = true",
{"disabled": True},
False,
id="disabled-true",
),
],
)
def test_client_config_dict_round_trip_keeps_tls(
toml_tls: str,
tls_dict: dict[str, bool] | None,
connect_tls: bool | TLSConfig | None,
):
"""Test that dictionary round trips preserve TLS and the connect config."""
config = ClientConfig.load(
config_source=f"[profile.default]\naddress = 'some-address'\n{toml_tls}"
)
expected_profile: dict[str, Any] = {"address": "some-address"}
if tls_dict is not None:
expected_profile["tls"] = tls_dict

config_dict = config.to_dict()
assert config_dict == {"default": expected_profile}

new_config = ClientConfig.from_dict(config_dict)
assert new_config == config
connect_config = new_config.profiles["default"].to_client_connect_config()
assert connect_config == config.profiles["default"].to_client_connect_config()
if connect_tls is None:
assert "tls" not in connect_config
else:
assert connect_config.get("tls") == connect_tls


def test_grpc_metadata_normalization_from_toml():
"""Test that gRPC metadata keys get normalized from TOML."""
toml_config = textwrap.dedent(
Expand Down
Loading