From c7c89722dfe0eb2da9e2eaa79268d4f96c343729 Mon Sep 17 00:00:00 2001 From: "Gary T. Giesen" Date: Fri, 10 Jul 2026 13:36:16 -0400 Subject: [PATCH] Fix sysrc.managed TypeError in test mode with non-string values In test mode, managed() built the preview message via string concatenation (name + " = " + value + ...). When YAML coerces an unquoted value such as YES into a Python bool, this raised TypeError: can only concatenate str (not "bool") to str. Use an f-string so the value is implicitly coerced to its string form. Output is byte-identical for string values. Fixes #60048 --- changelog/60048.fixed.md | 1 + salt/states/sysrc.py | 2 +- tests/pytests/unit/states/test_sysrc.py | 53 +++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 changelog/60048.fixed.md diff --git a/changelog/60048.fixed.md b/changelog/60048.fixed.md new file mode 100644 index 000000000000..1affd975215f --- /dev/null +++ b/changelog/60048.fixed.md @@ -0,0 +1 @@ +Fixed sysrc.managed raising TypeError in test mode when the value is a non-string (e.g. YAML coerces an unquoted YES to a bool) diff --git a/salt/states/sysrc.py b/salt/states/sysrc.py index 4c7b0d2a57f9..65502fca61e2 100644 --- a/salt/states/sysrc.py +++ b/salt/states/sysrc.py @@ -54,7 +54,7 @@ def managed(name, value, **kwargs): ret["comment"] = f'The value of "{name}" will be changed!' ret["changes"] = { "old": current_state, - "new": name + " = " + value + " will be set.", + "new": f"{name} = {value} will be set.", } # When test=true return none diff --git a/tests/pytests/unit/states/test_sysrc.py b/tests/pytests/unit/states/test_sysrc.py index 9126c0722482..23483b9a914f 100644 --- a/tests/pytests/unit/states/test_sysrc.py +++ b/tests/pytests/unit/states/test_sysrc.py @@ -45,6 +45,59 @@ def test_managed(): assert sysrc.managed("salt", "stack") == ret +def test_managed_test_mode_bool_value_60048(): + """ + Test mode must not raise TypeError when the value is a non-string + (e.g. YAML coerces an unquoted ``YES`` to the bool ``True``). + + Regression test for #60048. + """ + # sysrc.get returns None -> variable not yet set, so managed() falls + # through to the __opts__["test"] preview branch. + mock_get = MagicMock(return_value=None) + with patch.dict(sysrc.__salt__, {"sysrc.get": mock_get}): + # __opts__["test"] = True exercises the test-mode preview path. + with patch.dict(sysrc.__opts__, {"test": True}): + ret = sysrc.managed("vm_enable", True) + assert ret["result"] is None + assert ret["comment"] == 'The value of "vm_enable" will be changed!' + assert ret["changes"] == { + "old": None, + "new": "vm_enable = True will be set.", + } + + +def test_managed_test_mode_string_value_60048(): + """ + Inverse of the #60048 regression: a normal string value must render + identically before and after the f-string change, so this passes both + with and without the fix and guards against an output regression. + """ + mock_get = MagicMock(return_value=None) + with patch.dict(sysrc.__salt__, {"sysrc.get": mock_get}): + with patch.dict(sysrc.__opts__, {"test": True}): + ret = sysrc.managed("syslogd_flags", "-ss") + assert ret["result"] is None + assert ret["changes"] == { + "old": None, + "new": "syslogd_flags = -ss will be set.", + } + + +def test_managed_test_mode_int_value_60048(): + """ + Peripheral coverage for the test-mode preview: an integer value + (e.g. YAML ``value: 22``) is coerced to its string form rather than + raising TypeError. + """ + mock_get = MagicMock(return_value=None) + with patch.dict(sysrc.__salt__, {"sysrc.get": mock_get}): + with patch.dict(sysrc.__opts__, {"test": True}): + ret = sysrc.managed("sshd_port", 22) + assert ret["result"] is None + assert ret["changes"]["new"] == "sshd_port = 22 will be set." + + def test_absent(): """ Test to ensure a sysrc variable is absent.