Skip to content

Commit 978bdc6

Browse files
committed
Add tests for the new options
This commit also restructure the tests a bit because using all combination for all options explodes very quickly, so instead we just have a few meaningful combinations. We also do a small refactoring of the code to make it easier to mock. Signed-off-by: Leandro Lucarella <[email protected]>
1 parent 909ccad commit 978bdc6

File tree

2 files changed

+203
-40
lines changed

2 files changed

+203
-40
lines changed

src/frequenz/client/base/channel.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -236,14 +236,18 @@ def _get_contents(
236236
case bytes() as default_bytes:
237237
return default_bytes
238238
case pathlib.Path() as file_path:
239-
pass
239+
return _read_bytes(name, file_path)
240240
case unexpected:
241241
assert_never(unexpected)
242242
case pathlib.Path() as file_path:
243-
pass
243+
return _read_bytes(name, file_path)
244244
case unexpected:
245245
assert_never(unexpected)
246+
247+
248+
def _read_bytes(name: str, source: pathlib.Path) -> bytes:
249+
"""Read the contents of a file as bytes."""
246250
try:
247-
return file_path.read_bytes()
251+
return source.read_bytes()
248252
except OSError as exc:
249-
raise ValueError(f"Failed to read {name} from '{file_path}': {exc}") from exc
253+
raise ValueError(f"Failed to read {name} from '{source}': {exc}") from exc

tests/test_channel.py

Lines changed: 195 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,56 +3,168 @@
33

44
"""Test cases for the channel module."""
55

6+
import dataclasses
7+
import pathlib
68
from unittest import mock
79

810
import pytest
911
from grpc import ssl_channel_credentials
1012
from grpc.aio import Channel
1113

12-
from frequenz.client.base.channel import ChannelOptions, SslOptions, parse_grpc_uri
13-
14-
VALID_URLS = [
15-
("grpc://localhost", "localhost", 9090, True),
16-
("grpc://localhost:1234", "localhost", 1234, True),
17-
("grpc://localhost:1234?ssl=true", "localhost", 1234, True),
18-
("grpc://localhost:1234?ssl=false", "localhost", 1234, False),
19-
("grpc://localhost:1234?ssl=1", "localhost", 1234, True),
20-
("grpc://localhost:1234?ssl=0", "localhost", 1234, False),
21-
("grpc://localhost:1234?ssl=on", "localhost", 1234, True),
22-
("grpc://localhost:1234?ssl=off", "localhost", 1234, False),
23-
("grpc://localhost:1234?ssl=TRUE", "localhost", 1234, True),
24-
("grpc://localhost:1234?ssl=FALSE", "localhost", 1234, False),
25-
("grpc://localhost:1234?ssl=ON", "localhost", 1234, True),
26-
("grpc://localhost:1234?ssl=OFF", "localhost", 1234, False),
27-
("grpc://localhost:1234?ssl=0&ssl=1", "localhost", 1234, True),
28-
("grpc://localhost:1234?ssl=1&ssl=0", "localhost", 1234, False),
29-
]
14+
from frequenz.client.base.channel import (
15+
ChannelOptions,
16+
SslOptions,
17+
_to_bool,
18+
parse_grpc_uri,
19+
)
20+
21+
22+
@dataclasses.dataclass(frozen=True, kw_only=True)
23+
class _ValidUrlTestCase:
24+
title: str
25+
uri: str
26+
expected_host: str
27+
expected_options: ChannelOptions
28+
defaults: ChannelOptions = ChannelOptions()
3029

3130

32-
@pytest.mark.parametrize("uri, host, port, ssl", VALID_URLS)
33-
@pytest.mark.parametrize(
34-
"default_port", [None, 9090, 1234], ids=lambda x: f"default_port={x}"
35-
)
3631
@pytest.mark.parametrize(
37-
"default_ssl", [None, True, False], ids=lambda x: f"default_ssl={x}"
32+
"case",
33+
[
34+
_ValidUrlTestCase(
35+
title="default",
36+
uri="grpc://localhost",
37+
expected_host="localhost",
38+
expected_options=ChannelOptions(
39+
port=9090,
40+
ssl=SslOptions(
41+
enabled=True,
42+
root_certificates=None,
43+
private_key=None,
44+
certificate_chain=None,
45+
),
46+
),
47+
),
48+
_ValidUrlTestCase(
49+
title="default no SSL defaults",
50+
uri="grpc://localhost",
51+
defaults=ChannelOptions(port=2355, ssl=SslOptions(enabled=False)),
52+
expected_host="localhost",
53+
expected_options=ChannelOptions(
54+
port=2355,
55+
ssl=SslOptions(
56+
enabled=False,
57+
root_certificates=None,
58+
private_key=None,
59+
certificate_chain=None,
60+
),
61+
),
62+
),
63+
_ValidUrlTestCase(
64+
title="default with SSL defaults",
65+
uri="grpc://localhost",
66+
defaults=ChannelOptions(
67+
port=2355,
68+
ssl=SslOptions(
69+
enabled=True,
70+
root_certificates=None,
71+
private_key=b"key",
72+
certificate_chain=pathlib.Path("/chain"),
73+
),
74+
),
75+
expected_host="localhost",
76+
expected_options=ChannelOptions(
77+
port=2355,
78+
ssl=SslOptions(
79+
enabled=True,
80+
root_certificates=None,
81+
private_key=b"key",
82+
certificate_chain=pathlib.Path("/chain"),
83+
),
84+
),
85+
),
86+
_ValidUrlTestCase(
87+
title="complete no defaults",
88+
uri="grpc://localhost:1234?ssl=1&ssl_root_certificates_path=/root_cert"
89+
"&ssl_private_key_path=/key&ssl_certificate_chain_path=/chain",
90+
expected_host="localhost",
91+
expected_options=ChannelOptions(
92+
port=1234,
93+
ssl=SslOptions(
94+
enabled=True,
95+
root_certificates=pathlib.Path("/root_cert"),
96+
private_key=pathlib.Path("/key"),
97+
certificate_chain=pathlib.Path("/chain"),
98+
),
99+
),
100+
),
101+
_ValidUrlTestCase(
102+
title="complete no defaults",
103+
uri="grpc://localhost:1234?ssl=1&ssl_root_certificates_path=/root_cert"
104+
"&ssl_private_key_path=/key&ssl_certificate_chain_path=/chain",
105+
defaults=ChannelOptions(
106+
port=4444,
107+
ssl=SslOptions(
108+
enabled=True,
109+
root_certificates=pathlib.Path("/root_cert_def"),
110+
private_key=b"key_def",
111+
certificate_chain=b"chain_def",
112+
),
113+
),
114+
expected_host="localhost",
115+
expected_options=ChannelOptions(
116+
port=1234,
117+
ssl=SslOptions(
118+
enabled=True,
119+
root_certificates=pathlib.Path("/root_cert"),
120+
private_key=pathlib.Path("/key"),
121+
certificate_chain=pathlib.Path("/chain"),
122+
),
123+
),
124+
),
125+
],
126+
ids=lambda case: case.title,
38127
)
39-
def test_parse_uri_ok( # pylint: disable=too-many-arguments,too-many-locals
40-
uri: str,
41-
host: str,
42-
port: int,
43-
ssl: bool,
44-
default_port: int | None,
45-
default_ssl: bool | None,
128+
def test_parse_uri_ok( # pylint: disable=too-many-locals
129+
case: _ValidUrlTestCase,
46130
) -> None:
47131
"""Test successful parsing of gRPC URIs using grpcio."""
132+
uri = case.uri
133+
defaults = case.defaults
134+
135+
expected_host = case.expected_host
136+
expected_options = case.expected_options
48137
expected_channel = mock.MagicMock(name="mock_channel", spec=Channel)
49138
expected_credentials = mock.MagicMock(
50139
name="mock_credentials", spec=ssl_channel_credentials
51140
)
52-
expected_port = port if f":{port}" in uri or default_port is None else default_port
53-
expected_ssl = ssl if "ssl" in uri or default_ssl is None else default_ssl
54-
55-
defaults = ChannelOptions(port=expected_port, ssl=SslOptions(enabled=expected_ssl))
141+
expected_port = (
142+
expected_options.port
143+
if f":{expected_options.port}" in uri or defaults.port is None
144+
else defaults.port
145+
)
146+
expected_ssl = (
147+
expected_options.ssl.enabled
148+
if "ssl=" in uri or defaults.ssl.enabled is None
149+
else defaults.ssl.enabled
150+
)
151+
expected_root_certificates = (
152+
expected_options.ssl.root_certificates
153+
if "ssl_root_certificates_path=" in uri
154+
or defaults.ssl.root_certificates is None
155+
else defaults.ssl.root_certificates
156+
)
157+
expected_private_key = (
158+
expected_options.ssl.private_key
159+
if "ssl_private_key_path=" in uri or defaults.ssl.private_key is None
160+
else defaults.ssl.private_key
161+
)
162+
expected_certificate_chain = (
163+
expected_options.ssl.certificate_chain
164+
if "ssl_certificate_chain_path=" in uri
165+
or defaults.ssl.certificate_chain is None
166+
else defaults.ssl.certificate_chain
167+
)
56168

57169
with (
58170
mock.patch(
@@ -67,20 +179,58 @@ def test_parse_uri_ok( # pylint: disable=too-many-arguments,too-many-locals
67179
"frequenz.client.base.channel.ssl_channel_credentials",
68180
return_value=expected_credentials,
69181
) as ssl_channel_credentials_mock,
182+
mock.patch(
183+
"frequenz.client.base.channel._read_bytes",
184+
return_value=b"contents",
185+
) as get_contents_mock,
70186
):
71187
channel = parse_grpc_uri(uri, defaults)
72188

73189
assert channel == expected_channel
74-
expected_target = f"{host}:{expected_port}"
190+
expected_target = f"{expected_host}:{expected_port}"
75191
if expected_ssl:
76-
ssl_channel_credentials_mock.assert_called_once_with(root_certificates=None)
192+
if isinstance(expected_root_certificates, pathlib.Path):
193+
get_contents_mock.assert_any_call(
194+
"root certificates",
195+
expected_root_certificates,
196+
)
197+
expected_root_certificates = b"contents"
198+
if isinstance(expected_private_key, pathlib.Path):
199+
get_contents_mock.assert_any_call(
200+
"private key",
201+
expected_private_key,
202+
)
203+
expected_private_key = b"contents"
204+
if isinstance(expected_certificate_chain, pathlib.Path):
205+
get_contents_mock.assert_any_call(
206+
"certificate chain",
207+
expected_certificate_chain,
208+
)
209+
expected_certificate_chain = b"contents"
210+
ssl_channel_credentials_mock.assert_called_once_with(
211+
root_certificates=expected_root_certificates,
212+
private_key=expected_private_key,
213+
certificate_chain=expected_certificate_chain,
214+
)
77215
secure_channel_mock.assert_called_once_with(
78216
expected_target, expected_credentials
79217
)
80218
else:
81219
insecure_channel_mock.assert_called_once_with(expected_target)
82220

83221

222+
@pytest.mark.parametrize("value", ["true", "on", "1", "TrUe", "On", "ON", "TRUE"])
223+
def test_to_bool_true(value: str) -> None:
224+
"""Test conversion of valid boolean values to True."""
225+
assert _to_bool(value) is True
226+
227+
228+
@pytest.mark.parametrize("value", ["false", "off", "0", "FaLsE", "Off", "OFF", "FALSE"])
229+
def test_to_bool_false(value: str) -> None:
230+
"""Test conversion of valid boolean values to False."""
231+
assert _to_bool(value) is False
232+
233+
84234
INVALID_URLS = [
85235
("http://localhost", "Invalid scheme 'http' in the URI, expected 'grpc'"),
86236
("grpc://", "Host name is missing in URI 'grpc://'"),
@@ -100,6 +250,15 @@ def test_parse_uri_ok( # pylint: disable=too-many-arguments,too-many-locals
100250
"grpc://localhost:1234?ssl=1&ffl=true",
101251
r"Unexpected query parameters \[ffl\]",
102252
),
253+
(
254+
"grpc://localhost:1234?ssl=0&ssl_root_certificates_path=/root&"
255+
"ssl_private_key_path=/key&ssl_certificate_chain_path=/chain",
256+
r"Option\(s\) ssl_root_certificates_path, ssl_private_key_path, "
257+
r"ssl_certificate_chain_path found in URI 'grpc://localhost:1234\?"
258+
r"ssl=0\&ssl_root_certificates_path=/root\&"
259+
r"ssl_private_key_path=/key\&ssl_certificate_chain_path=/chain', "
260+
r"but SSL is disabled",
261+
),
103262
]
104263

105264

0 commit comments

Comments
 (0)