33
44"""Test cases for the channel module."""
55
6+ import dataclasses
7+ import pathlib
68from unittest import mock
79
810import pytest
911from grpc import ssl_channel_credentials
1012from 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+
84234INVALID_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