Skip to content

Commit 48fbf78

Browse files
authored
ConversionUtils() unit tests and hardening (#472)
* Unit tests for ConversionUtils() + hardening 1. tests/unit/module_utils/common/test_conversion_utils.py Unit tests for ConversioniUtils() 2. plugins/module_utils/common/conversion.py 2a. translate_mac_address - Hardening - raise ValueError if anything but a string * translate_mac_address: use original mac in ValueError() 1. plugins/module_utils/common/conversion.py 1a. translate_mac_address was failing for non-string values. Fix by convering mac_addr to str before re.sub() Also, use the original mac_addr rather than the re.sub() version in the ValueError() message. 2. Fix two fabric unit tests Two unit tests were failing after the change in 1 above. These were in files: - tests/unit/modules/dcnm/dcnm_fabric/test_fabric_create.py - tests/unit/modules/dcnm/dcnm_fabric/test_fabric_create_bulk.py Fixed these to expect the original mac_addr rather than the re.sub() version. * UT: validate_fabric_name Adding final unit tests for ConversionUtils().validate_fabric_name
1 parent db277e8 commit 48fbf78

File tree

4 files changed

+361
-5
lines changed

4 files changed

+361
-5
lines changed

plugins/module_utils/common/conversion.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
__metaclass__ = type
1818
__author__ = "Allen Robel"
1919

20+
import copy
2021
import inspect
2122
import re
2223

@@ -146,10 +147,13 @@ def translate_mac_address(mac_addr):
146147
- On success, return translated mac address.
147148
- On failure, raise ``ValueError``.
148149
"""
149-
mac_addr = re.sub(r"[\W\s_]", "", mac_addr)
150+
mac_addr_orig = copy.copy(mac_addr)
151+
mac_addr = re.sub(r"[\W\s_]", "", str(mac_addr))
150152
if not re.search("^[A-Fa-f0-9]{12}$", mac_addr):
151-
raise ValueError(f"Invalid MAC address: {mac_addr}")
152-
return "".join((mac_addr[:4], ".", mac_addr[4:8], ".", mac_addr[8:]))
153+
msg = f"Invalid MAC address: {mac_addr_orig}"
154+
raise ValueError(msg)
155+
mac = "".join((mac_addr[:4], ".", mac_addr[4:8], ".", mac_addr[8:]))
156+
return mac.lower()
153157

154158
def validate_fabric_name(self, value):
155159
"""
Lines changed: 352 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,352 @@
1+
# Copyright (c) 2025 Cisco and/or its affiliates.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from __future__ import absolute_import, division, print_function
16+
17+
__metaclass__ = type
18+
19+
__copyright__ = "Copyright (c) 2025 Cisco and/or its affiliates."
20+
__author__ = "Allen Robel"
21+
22+
import re
23+
24+
import pytest
25+
from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import ConversionUtils
26+
from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import does_not_raise
27+
28+
RE_ASN_STR = "^(((\\+)?[1-9]{1}[0-9]{0,8}|(\\+)?[1-3]{1}[0-9]{1,9}|(\\+)?[4]"
29+
RE_ASN_STR += "{1}([0-1]{1}[0-9]{8}|[2]{1}([0-8]{1}[0-9]{7}|[9]{1}([0-3]{1}"
30+
RE_ASN_STR += "[0-9]{6}|[4]{1}([0-8]{1}[0-9]{5}|[9]{1}([0-5]{1}[0-9]{4}|[6]"
31+
RE_ASN_STR += "{1}([0-6]{1}[0-9]{3}|[7]{1}([0-1]{1}[0-9]{2}|[2]{1}([0-8]{1}"
32+
RE_ASN_STR += "[0-9]{1}|[9]{1}[0-5]{1})))))))))|([1-5]\\d{4}|[1-9]\\d{0,3}|6"
33+
RE_ASN_STR += "[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5])"
34+
RE_ASN_STR += "(\\.([1-5]\\d{4}|[1-9]\\d{0,3}|6[0-4]\\d{3}|65[0-4]"
35+
RE_ASN_STR += "\\d{2}|655[0-2]\\d|6553[0-5]|0))?)$"
36+
re_asn = re.compile(RE_ASN_STR)
37+
re_valid_fabric_name = re.compile(r"[a-zA-Z]+[a-zA-Z0-9_-]*")
38+
39+
40+
def test_conversion_utils_00000() -> None:
41+
"""
42+
Classes and Methods
43+
- ConversionUtils
44+
- __init__()
45+
46+
Test
47+
- Class attributes are initialized to expected values
48+
- Exception is not raised
49+
"""
50+
with does_not_raise():
51+
instance = ConversionUtils()
52+
assert instance.class_name == "ConversionUtils"
53+
assert instance.bgp_as_invalid_reason is None
54+
assert instance.re_asn == re_asn
55+
assert instance.re_valid_fabric_name == re_valid_fabric_name
56+
57+
58+
@pytest.mark.parametrize(
59+
"value, expected",
60+
[
61+
(0, False), # 2-byte and 4-byte ASN minimum exceeded
62+
(1, True), # 2-byte and 4-byte ASN minimum
63+
("65535", True), # 2-byte ASN maximum
64+
("65536", True), # 4-byte ASN within range
65+
("1.0", True), # dotted notation,same as 65536
66+
("65535.65535", True), # dotted notation, maximum
67+
("65535.65536", False), # dotted notation, maximum exceeded
68+
("4200000000", True), # 4-byte ASN private use minimum
69+
("4294967294", True), # 4-byte ASN private use maximum
70+
("4294967295", True), # 4-byte ASN maximum
71+
("4294967296", False), # 4-byte ASN maximum exceeded
72+
("asdf", False), # fails regex
73+
(None, False), # fails regex
74+
],
75+
)
76+
def test_conversion_utils_00010(value, expected) -> None:
77+
"""
78+
### Classes and Methods
79+
80+
- ConversionUtils()
81+
- __init__()
82+
- bgp_as_is_valid()
83+
84+
### Summary
85+
Verify valid BGP AS is accepted and invalid BGP AS is rejected.
86+
87+
### Setup
88+
89+
- ConversionUtils() is instantiated
90+
91+
### Test
92+
93+
- ``bgp_as_is_valid`` is called with various valid and invalid values.
94+
95+
### Expected Result
96+
- Exceptions are never raised
97+
- Appropriate return value (True or False) for each value.
98+
"""
99+
with does_not_raise():
100+
instance = ConversionUtils()
101+
assert instance.bgp_as_is_valid(value) == expected
102+
103+
104+
@pytest.mark.parametrize(
105+
"value, expected",
106+
[
107+
(0, 0), # Not a boolean
108+
(1, 1), # Not a boolean
109+
("foo", "foo"), # Not a boolean
110+
(True, True), # boolean
111+
(False, False), # boolean
112+
("True", True), # boolean string representation
113+
("False", False), # boolean string representation
114+
("true", True), # boolean string representation
115+
("false", False), # boolean string representation
116+
("yes", True), # boolean string representation
117+
("no", False), # boolean string representation
118+
("YES", True), # boolean string representation
119+
("NO", False), # boolean string representation
120+
(None, None), # Not a boolean
121+
],
122+
)
123+
def test_conversion_utils_00020(value, expected) -> None:
124+
"""
125+
### Classes and Methods
126+
127+
- ConversionUtils()
128+
- __init__()
129+
- make_boolean()
130+
131+
### Summary
132+
Verify expected values are returned.
133+
134+
### Setup
135+
136+
- ConversionUtils() is instantiated
137+
138+
### Test
139+
140+
- ``make_boolean`` is called with various values.
141+
142+
### Expected Result
143+
- Exceptions are never raised
144+
- Appropriate return value (True, False, or value) for each value.
145+
"""
146+
with does_not_raise():
147+
instance = ConversionUtils()
148+
assert instance.make_boolean(value) == expected
149+
150+
151+
@pytest.mark.parametrize(
152+
"value, expected",
153+
[
154+
(0, 0), # int
155+
(999.1, 999), # float converted to int
156+
("foo", "foo"), # Not an int
157+
(True, True), # Not an int
158+
(False, False), # Not an int
159+
("True", "True"), # Not an int
160+
("False", "False"), # Not an int
161+
(None, None), # Not an int
162+
],
163+
)
164+
def test_conversion_utils_00030(value, expected) -> None:
165+
"""
166+
### Classes and Methods
167+
168+
- ConversionUtils()
169+
- __init__()
170+
- make_int()
171+
172+
### Summary
173+
Verify expected values are returned.
174+
175+
### Setup
176+
177+
- ConversionUtils() is instantiated
178+
179+
### Test
180+
181+
- ``make_int`` is called with various values.
182+
183+
### Expected Result
184+
- Exceptions are never raised
185+
- Appropriate return value for each input value.
186+
"""
187+
with does_not_raise():
188+
instance = ConversionUtils()
189+
assert instance.make_int(value) == expected
190+
191+
192+
@pytest.mark.parametrize(
193+
"value, expected",
194+
[
195+
(0, 0), # int
196+
(999.1, 999.1), # float
197+
("foo", "foo"), # str, not representation of None
198+
(True, True), # bool, not representation of None
199+
(False, False), # bool, not representation of None
200+
("", None), # str, empty string converted to None
201+
("none", None), # str, representation of None
202+
("null", None), # str, representation of None
203+
(None, None), # None
204+
],
205+
)
206+
def test_conversion_utils_00040(value, expected) -> None:
207+
"""
208+
### Classes and Methods
209+
210+
- ConversionUtils()
211+
- __init__()
212+
- make_none()
213+
214+
### Summary
215+
Verify expected values are returned.
216+
217+
### Setup
218+
219+
- ConversionUtils() is instantiated
220+
221+
### Test
222+
223+
- `make_none` is called with various values.
224+
225+
### Expected Result
226+
- Exceptions are never raised
227+
- Appropriate return value for each input value.
228+
"""
229+
with does_not_raise():
230+
instance = ConversionUtils()
231+
assert instance.make_none(value) == expected
232+
233+
234+
@pytest.mark.parametrize(
235+
"value, expected, raises",
236+
[
237+
("aaaa.bbbb.cccc", "aaaa.bbbb.cccc", False), # valid mac
238+
("aaaabbbbcccc", "aaaa.bbbb.cccc", False), # valid mac
239+
("aa:aa:bb:bb:cc:cc", "aaaa.bbbb.cccc", False), # valid mac
240+
("aa-aa-bb-bb-cc-cc", "aaaa.bbbb.cccc", False), # valid mac
241+
("Aa-AA-BB-bb-cC-cc", "aaaa.bbbb.cccc", False), # valid mac
242+
("zaaabbbbcccc", None, True), # invalid mac
243+
("notamac", None, True), # invalid mac
244+
(0, None, True), # invalid mac
245+
(999.1, None, True), # invalid mac
246+
("", None, True), # invalid mac
247+
(True, None, True), # invalid mac
248+
(False, None, True), # invalid mac
249+
(None, None, True), # invalid mac
250+
],
251+
)
252+
def test_conversion_utils_00050(value, expected, raises) -> None:
253+
"""
254+
### Classes and Methods
255+
256+
- ConversionUtils()
257+
- __init__()
258+
- translate_mac_address()
259+
260+
### Summary
261+
262+
- Verify expected values are returned for valid mac
263+
- Verify ValueError is raised for invalid mac
264+
265+
### Setup
266+
267+
- ConversionUtils() is instantiated
268+
269+
### Test
270+
271+
- `translate_mac_address` is called with various values.
272+
273+
### Expected Result
274+
275+
- dotted-quad mac address returned for valid mac
276+
- ValueError raised for invalid mac
277+
"""
278+
with does_not_raise():
279+
instance = ConversionUtils()
280+
if not raises:
281+
with does_not_raise():
282+
assert instance.translate_mac_address(value) == expected
283+
else:
284+
match = f"Invalid MAC address: {str(value)}"
285+
with pytest.raises(ValueError, match=match):
286+
instance.translate_mac_address(value)
287+
288+
289+
@pytest.mark.parametrize(
290+
"value, expected_exception",
291+
[
292+
("validFabric", None), # valid fabric name
293+
("ValidFabric123", None), # valid fabric name with numbers
294+
("fabric_name", None), # valid fabric name with underscore
295+
("fabric-name", None), # valid fabric name with dash
296+
("a", None), # valid single letter
297+
("A1_2-3", None), # valid with all allowed characters
298+
("123fabric", ValueError), # invalid - starts with number
299+
("_fabric", ValueError), # invalid - starts with underscore
300+
("-fabric", ValueError), # invalid - starts with dash
301+
("", ValueError), # invalid - empty string
302+
("fabric@name", ValueError), # invalid - contains special character
303+
("fabric name", ValueError), # invalid - contains space
304+
("fabric.name", ValueError), # invalid - contains dot
305+
(123, TypeError), # invalid - not a string
306+
(None, TypeError), # invalid - not a string
307+
(True, TypeError), # invalid - not a string
308+
([], TypeError), # invalid - not a string
309+
],
310+
)
311+
def test_conversion_utils_00060(value, expected_exception) -> None:
312+
"""
313+
### Classes and Methods
314+
315+
- ConversionUtils()
316+
- __init__()
317+
- validate_fabric_name()
318+
319+
### Summary
320+
321+
- Verify valid fabric names pass validation
322+
- Verify TypeError is raised for non-string values
323+
- Verify ValueError is raised for invalid fabric name patterns
324+
325+
### Setup
326+
327+
- ConversionUtils() is instantiated
328+
329+
### Test
330+
331+
- `validate_fabric_name` is called with various values.
332+
333+
### Expected Result
334+
335+
- No exception raised for valid fabric names
336+
- TypeError raised for non-string values
337+
- ValueError raised for invalid fabric name patterns
338+
"""
339+
with does_not_raise():
340+
instance = ConversionUtils()
341+
342+
if expected_exception is None:
343+
with does_not_raise():
344+
instance.validate_fabric_name(value)
345+
elif expected_exception == TypeError:
346+
match = f"Invalid fabric name. Expected string. Got {re.escape(str(value))}."
347+
with pytest.raises(TypeError, match=match):
348+
instance.validate_fabric_name(value)
349+
elif expected_exception == ValueError:
350+
match = f"Invalid fabric name: {value}. Fabric name must start with a letter A-Z or a-z and contain only the characters in: \\[A-Z,a-z,0-9,-,_\\]."
351+
with pytest.raises(ValueError, match=match):
352+
instance.validate_fabric_name(value)

tests/unit/modules/dcnm/dcnm_fabric/test_fabric_create.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -623,7 +623,7 @@ def responses():
623623
match = r"FabricCreate\._fixup_anycast_gw_mac: "
624624
match += "Error translating ANYCAST_GW_MAC for fabric f1, "
625625
match += "ANYCAST_GW_MAC: 00:12:34:56:78:9, "
626-
match += "Error detail: Invalid MAC address: 00123456789"
626+
match += "Error detail: Invalid MAC address: 00:12:34:56:78:9"
627627

628628
with pytest.raises(ValueError, match=match):
629629
instance.commit()

tests/unit/modules/dcnm/dcnm_fabric/test_fabric_create_bulk.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -640,7 +640,7 @@ def responses():
640640
match = r"FabricCreateBulk\._fixup_anycast_gw_mac: "
641641
match += "Error translating ANYCAST_GW_MAC for fabric f1, "
642642
match += "ANYCAST_GW_MAC: 00:12:34:56:78:9, "
643-
match += "Error detail: Invalid MAC address: 00123456789"
643+
match += "Error detail: Invalid MAC address: 00:12:34:56:78:9"
644644

645645
with pytest.raises(ValueError, match=match):
646646
instance.commit()

0 commit comments

Comments
 (0)