/
githubmirror
/
salt
Обзор
Документация
Войти
/
githubmirror
/
salt
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
tests/pytests/unit/states/test_user.py
562 строки
18 KB
Daniel A. Wozniak
Allow user.present to be test-evaluated with not-yet-existing groups
18 июн 2026, 19:58
18 июн 2026, 19:58
03d8d00
Код
Авторство
О чём код?
""" :codeauthor: Rahul Handay <rahulha@saltstack.com> """ import logging import pytest import salt.states.user as user import salt.utils.platform from tests.support.mock import MagicMock, Mock, patch log = logging.getLogger(__name__) @pytest.fixture def configure_loader_modules(): return {user: {}} def test_present(): """ Test to ensure that the named user is present with the specified properties """ ret = {"name": "salt", "changes": {}, "result": False, "comment": ""} mock_false = MagicMock(return_value=False) mock_empty_list = MagicMock(return_value=[]) with patch.dict(user.__grains__, {"kernel": "Linux"}): with patch.dict( user.__salt__, { "group.info": mock_false, "user.info": mock_empty_list, "user.chkey": mock_empty_list, "user.add": mock_false, }, ): ret.update({"comment": "The following group(s) are not present: salt"}) assert user.present("salt", groups=["salt"]) == ret mock_false = MagicMock( side_effect=[ {"key": "value"}, {"key": "value"}, {"key": "value"}, False, False, ] ) with patch.object(user, "_changes", mock_false): with patch.dict(user.__opts__, {"test": True}): ret.update( { "comment": ( "The following user attributes are set to be changed:\n" "key: value\n" ), "result": None, } ) assert user.present("salt") == ret with patch.dict(user.__opts__, {"test": False}): comment = "These values could not be changed: {!r}".format( {"key": "value"} ) ret.update({"comment": comment, "result": False}) assert user.present("salt") == ret with patch.dict(user.__opts__, {"test": True}): ret.update( {"comment": "User salt set to be added", "result": None} ) assert user.present("salt") == ret with patch.dict(user.__opts__, {"test": False}): ret.update( { "comment": "Failed to create new user salt", "result": False, } ) assert user.present("salt") == ret def test_present_invalid_uid_change(): mock_info = MagicMock( side_effect=[ { "uid": 5000, "gid": 5000, "groups": ["foo"], "home": "/home/foo", "fullname": "Foo Bar", } ] ) dunder_salt = { "user.info": mock_info, "file.group_to_gid": MagicMock(side_effect=["foo"]), "file.gid_to_group": MagicMock(side_effect=[5000, 5000]), } with patch.dict(user.__grains__, {"kernel": "Linux"}), patch.dict( user.__salt__, dunder_salt ): ret = user.present("foo", uid=5001) assert not ret["result"] assert ret["comment"].count("not permitted") == 1 def test_present_invalid_gid_change(): mock_info = MagicMock( side_effect=[ { "uid": 5000, "gid": 5000, "groups": ["foo"], "home": "/home/foo", "fullname": "Foo Bar", } ] ) dunder_salt = { "user.info": mock_info, "file.group_to_gid": MagicMock(return_value="foo"), "file.gid_to_group": MagicMock(return_value=5000), } with patch.dict(user.__grains__, {"kernel": "Linux"}), patch.dict( user.__salt__, dunder_salt ): ret = user.present("foo", gid=5001) assert not ret["result"] assert ret["comment"].count("not permitted") == 1 def test_present_invalid_uid_gid_change(): mock_info = MagicMock( side_effect=[ { "uid": 5000, "gid": 5000, "groups": ["foo"], "home": "/home/foo", "fullname": "Foo Bar", } ] ) dunder_salt = { "user.info": mock_info, "file.group_to_gid": MagicMock(return_value="foo"), "file.gid_to_group": MagicMock(return_value=5000), } with patch.dict(user.__grains__, {"kernel": "Linux"}), patch.dict( user.__salt__, dunder_salt ): ret = user.present("foo", uid=5001, gid=5001) assert not ret["result"] assert ret["comment"].count("not permitted") == 2 def test_present_uid_gid_change(): before = { "uid": 5000, "gid": 5000, "groups": ["foo"], "home": "/home/foo", "fullname": "Foo Bar", } after = { "uid": 5001, "gid": 5001, "groups": ["othergroup"], "home": "/home/foo", "fullname": "Foo Bar", } # user.info should be called 4 times. Once the first time that # _changes() is called, once before and after changes are applied (to # get the before/after for the changes dict, and one last time to # confirm that no changes still need to be made. mock_info = MagicMock(side_effect=[before, before, after, after]) mock_group_to_gid = MagicMock(side_effect=[5000, 5000, 5001, 5001]) mock_gid_to_group = MagicMock( side_effect=["othergroup", "foo", "othergroup", "othergroup"] ) dunder_salt = { "user.info": mock_info, "user.chuid": Mock(), "user.chgid": Mock(), "file.group_to_gid": mock_group_to_gid, "file.gid_to_group": mock_gid_to_group, "group.info": MagicMock(return_value=after), "user.chgroups": MagicMock(return_value=True), } with patch.dict(user.__grains__, {"kernel": "Linux"}), patch.dict( user.__salt__, dunder_salt ), patch.dict(user.__opts__, {"test": False}), patch( "os.path.isdir", MagicMock(return_value=True) ): ret = user.present( "foo", uid=5001, gid=5001, allow_uid_change=True, allow_gid_change=True ) assert ret == { "comment": "Updated user foo", "changes": {"gid": 5001, "uid": 5001, "groups": []}, "name": "foo", "result": True, } def test_absent(): """ Test to ensure that the named user is absent """ ret = {"name": "salt", "changes": {}, "result": None, "comment": ""} mock = MagicMock(side_effect=[True, True, False]) mock1 = MagicMock(return_value=False) with patch.dict( user.__salt__, {"user.info": mock, "user.delete": mock1, "group.info": mock1}, ): with patch.dict(user.__opts__, {"test": True}): ret.update({"comment": "User salt set for removal"}) assert user.absent("salt") == ret with patch.dict(user.__opts__, {"test": False}): ret.update({"comment": "Failed to remove user salt", "result": False}) assert user.absent("salt") == ret ret.update({"comment": "User salt is not present", "result": True}) assert user.absent("salt") == ret def test_changes(): """ Test salt.states.user._changes """ mock_info = MagicMock( return_value={ "uid": 5000, "gid": 5000, "groups": ["foo"], "home": "/home/foo", "fullname": "Foo Bar", } ) shadow_info = MagicMock( return_value={"min": 2, "max": 88888, "inact": 77, "warn": 14} ) shadow_hash = MagicMock(return_value="abcd") dunder_salt = { "user.info": mock_info, "shadow.info": shadow_info, "shadow.default_hash": shadow_hash, "file.group_to_gid": MagicMock(side_effect=["foo"]), "file.gid_to_group": MagicMock(side_effect=[5000, 5000]), } with patch.dict(user.__grains__, {"kernel": "Linux"}), patch.dict( user.__salt__, dunder_salt ), patch.dict(user.__opts__, {"test": False}), patch( "os.path.isdir", MagicMock(return_value=True) ): ret = user._changes("foo", maxdays=999999, inactdays=0, warndays=7) assert ret == { "maxdays": 999999, "mindays": 0, "fullname": "", "warndays": 7, "inactdays": 0, } def test_gecos_field_changes_in_user_present(): """ Test if the gecos fields change in salt.states.user.present """ shadow_info = MagicMock( return_value={"min": 2, "max": 88888, "inact": 77, "warn": 14, "passwd": ""} ) shadow_hash = MagicMock(return_value="abcd") mock_info = MagicMock( side_effect=[ { "uid": 5000, "gid": 5000, "groups": ["foo"], "home": "/home/foo", "fullname": "Foo Bar", "homephone": "667788", }, { "uid": 5000, "gid": 5000, "groups": ["foo"], "home": "/home/foo", "fullname": "Bar Bar", "homephone": "44566", }, ] ) mock_changes = MagicMock(side_effect=[{"homephone": "667788"}, None]) dunder_salt = { "user.info": mock_info, "user.chhomephone": MagicMock(return_value=True), "shadow.info": shadow_info, "shadow.default_hash": shadow_hash, "file.group_to_gid": MagicMock(side_effect=["foo"]), "file.gid_to_group": MagicMock(side_effect=[5000, 5000]), } with patch.dict(user.__grains__, {"kernel": "Linux"}), patch.dict( user.__salt__, dunder_salt ), patch.dict(user.__opts__, {"test": False}), patch.object( user, "_changes", mock_changes ): res = user.present("Foo", homephone=44566, fullname="Bar Bar") assert res["changes"] == {"homephone": "44566", "fullname": "Bar Bar"} def test_present_password_lock_test_mode(): ret = { "name": "salt", "changes": {}, "result": True, "comment": "User salt is present and up to date", } mock_info = MagicMock( return_value={ "uid": 5000, "gid": 5000, "groups": [], "home": "/home/salt", "fullname": "Salty McSalterson", } ) shadow_info = MagicMock( side_effect=[ {"min": 2, "max": 88888, "inact": 77, "warn": 14, "passwd": "!"}, {"min": 2, "max": 88888, "inact": 77, "warn": 14, "passwd": ""}, ] ) shadow_hash = MagicMock(return_value="abcd") with patch.dict(user.__grains__, {"kernel": "Linux"}), patch.dict( user.__salt__, { "shadow.default_hash": shadow_hash, "shadow.info": shadow_info, "user.info": mock_info, "file.gid_to_group": MagicMock(return_value=5000), }, ), patch.dict(user.__opts__, {"test": True}): assert user.present("salt", createhome=False, password_lock=True) == ret ret.update( { "comment": "The following user attributes are set to be changed:\npassword_lock: True\n" } ) ret.update({"result": None}) assert user.present("salt", createhome=False, password_lock=True) == ret def test_present_password_lock(): ret = { "name": "salt", "changes": {"passwd": "XXX-REDACTED-XXX"}, "result": True, "comment": "Updated user salt", } mock_info = MagicMock( return_value={ "uid": 5000, "gid": 5000, "groups": [], "home": "/home/salt", "fullname": "Salty McSalterson", } ) shadow_info = MagicMock( side_effect=[ {"min": 2, "max": 88888, "inact": 77, "warn": 14, "passwd": ""}, {"min": 2, "max": 88888, "inact": 77, "warn": 14, "passwd": ""}, {"min": 2, "max": 88888, "inact": 77, "warn": 14, "passwd": "!"}, {"min": 2, "max": 88888, "inact": 77, "warn": 14, "passwd": "!"}, ] ) shadow_hash = MagicMock(return_value="abcd") unlock_account = MagicMock() unlock_password = MagicMock() lock_password = MagicMock() with patch.dict(user.__grains__, {"kernel": "Linux"}), patch.dict( user.__salt__, { "shadow.default_hash": shadow_hash, "shadow.info": shadow_info, "user.info": mock_info, "file.gid_to_group": MagicMock(return_value=5000), "shadow.unlock_account": unlock_account, "shadow.unlock_password": unlock_password, "shadow.lock_password": lock_password, }, ), patch.dict(user.__opts__, {"test": False}): assert user.present("salt", createhome=False, password_lock=True) == ret unlock_password.assert_not_called() unlock_account.assert_not_called() if salt.utils.platform.is_windows(): lock_password.assert_not_called() else: lock_password.assert_called_once() def test_present_password_unlock(): ret = { "name": "salt", "changes": {"passwd": "XXX-REDACTED-XXX"}, "result": True, "comment": "Updated user salt", } mock_info = MagicMock( return_value={ "uid": 5000, "gid": 5000, "groups": [], "home": "/home/salt", "fullname": "Salty McSalterson", } ) shadow_info = MagicMock( side_effect=[ {"min": 2, "max": 88888, "inact": 77, "warn": 14, "passwd": "!"}, {"min": 2, "max": 88888, "inact": 77, "warn": 14, "passwd": "!"}, {"min": 2, "max": 88888, "inact": 77, "warn": 14, "passwd": ""}, {"min": 2, "max": 88888, "inact": 77, "warn": 14, "passwd": ""}, ] ) shadow_hash = MagicMock(return_value="abcd") unlock_account = MagicMock() unlock_password = MagicMock() lock_password = MagicMock() with patch.dict(user.__grains__, {"kernel": "Linux"}), patch.dict( user.__salt__, { "shadow.default_hash": shadow_hash, "shadow.info": shadow_info, "user.info": mock_info, "file.gid_to_group": MagicMock(return_value=5000), "shadow.unlock_account": unlock_account, "shadow.unlock_password": unlock_password, "shadow.lock_password": lock_password, }, ), patch.dict(user.__opts__, {"test": False}): assert user.present("salt", createhome=False, password_lock=False) == ret lock_password.assert_not_called() if salt.utils.platform.is_windows(): unlock_account.assert_called_once() unlock_password.assert_not_called() else: unlock_password.assert_called_once() unlock_account.assert_not_called() def test_present_test_mode_missing_group_new_user(): """ Regression test for #68110: user.present should not fail in test mode when a referenced group does not yet exist (e.g. it will be created by a group.present requisite during the actual run). When the user also does not yet exist, the state should report it would be added and that the missing groups are pending. """ mock_false = MagicMock(return_value=False) mock_empty_list = MagicMock(return_value=[]) with patch.dict(user.__grains__, {"kernel": "Linux"}), patch.dict( user.__salt__, { "group.info": mock_false, "user.info": mock_empty_list, "user.chkey": mock_empty_list, "user.add": mock_false, }, ), patch.dict(user.__opts__, {"test": True}): ret = user.present("foo", groups=["foo"]) assert ret["result"] is None, ret assert "set to be added" in ret["comment"], ret assert "foo" in ret["comment"], ret def test_present_test_mode_missing_group_existing_user(): """ Regression test for #68110: when the user already exists, a missing required group should not cause a hard failure in test mode. The pending group(s) should appear in the test-mode comment. """ mock_info = MagicMock( return_value={ "uid": 5000, "gid": 5000, "groups": ["bar"], "home": "/home/baz", "fullname": "Baz", } ) # group.info: "foo" missing, "bar" exists. group_info = MagicMock( side_effect=lambda name: False if name == "foo" else {"gid": 5000} ) dunder_salt = { "group.info": group_info, "user.info": mock_info, "file.group_to_gid": MagicMock(return_value=5000), "file.gid_to_group": MagicMock(return_value="bar"), } with patch.dict(user.__grains__, {"kernel": "Linux"}), patch.dict( user.__salt__, dunder_salt ), patch.dict(user.__opts__, {"test": True}): ret = user.present("baz", groups=["foo", "bar"]) assert ret["result"] is not False, ret assert "not present" not in ret["comment"], ret # Pending group should be reported somewhere in the comment. assert "foo" in ret["comment"], ret @pytest.mark.parametrize( "current,wanted,remove,return_value,expected", [ (["grp1"], ["grp1"], False, MagicMock(return_value={"gid": 100}), False), ( ["grp1"], ["grp1", "grp2"], False, MagicMock(side_effect=[{"gid": 100}, {"gid": 200}]), True, ), ( ["grp1"], ["grp1", "grp2"], False, MagicMock(side_effect=[{"gid": 100}, {"gid": 100}]), False, ), ( ["grp1", "grp2"], ["grp1"], True, MagicMock(side_effect=[{"gid": 100}, {"gid": 200}]), True, ), ( ["grp1", "grp2"], ["grp1"], True, MagicMock(side_effect=[{"gid": 100}, {"gid": 100}]), False, ), ], ) def test__group_changes(current, wanted, remove, return_value, expected): with patch.dict(user.__salt__, {"group.info": return_value}): ret = user._group_changes(current, wanted, remove) assert ret == expected