Files
ansible/test/units/_internal/_encryption/test_crypt.py
David Shrewsbury 83d2ce771c Modify crypt library import pattern (#86120)
Hide the functionality of the _internal/_encryption/_crypt.py module
behind an object so that we don't have code executed at import time.
2025-11-11 17:33:45 -05:00

150 lines
6.4 KiB
Python

from __future__ import annotations
import errno
import pytest
from pytest_mock import MockerFixture
from ansible._internal._encryption._crypt import _CryptLib, CryptFacade, _FAILURE_TOKENS
class TestCryptFacade:
def test_unsupported_platform(self, mocker: MockerFixture) -> None:
"""Test that unsupported platforms are skipped."""
mock_libs = (
_CryptLib('foo', include_platforms=frozenset({'fake_platform'})),
)
mocker.patch('ansible._internal._encryption._crypt._CRYPT_LIBS', mock_libs)
with pytest.raises(ImportError, match=r'Cannot find crypt implementation'):
CryptFacade()
def test_libc_fallback(self, mocker: MockerFixture) -> None:
"""Test that a library name of None will load the libc library."""
mock_libs = (
_CryptLib(None),
)
mocker.patch('ansible._internal._encryption._crypt._CRYPT_LIBS', mock_libs)
load_lib_mock = mocker.patch('ctypes.cdll.LoadLibrary')
crypt_facade = CryptFacade()
load_lib_mock.assert_called_once_with(None)
assert crypt_facade._crypt_name is None
def test_library_with_no_crypt_methods(self, mocker: MockerFixture) -> None:
"""Test that a library without crypt() and crypt_r() is skipped."""
mock_libs = (
_CryptLib(None),
)
class MockCDLL:
pass
mocker.patch('ansible._internal._encryption._crypt._CRYPT_LIBS', mock_libs)
mocker.patch('ctypes.cdll.LoadLibrary', return_value=MockCDLL())
with pytest.raises(ImportError, match=r'Cannot find crypt implementation'):
CryptFacade()
def test_library_with_no_crypt_r_or_crypt_gensalt_rn(self, mocker: MockerFixture) -> None:
"""Test that a library without crypt_r() or crypt_gensalt_rn() is prepped correctly."""
mock_libs = (
_CryptLib(None),
)
class MockCDLL:
class MockCrypt:
def __init__(self):
self.argtypes = None
self.restype = None
def __init__(self):
self.crypt = self.MockCrypt()
self.crypt_gensalt = self.MockCrypt()
mocker.patch('ansible._internal._encryption._crypt._CRYPT_LIBS', mock_libs)
mocker.patch('ctypes.cdll.LoadLibrary', return_value=MockCDLL())
crypt_facade = CryptFacade()
assert crypt_facade._crypt_impl is not None
assert crypt_facade._crypt_impl.argtypes is not None
assert crypt_facade._crypt_impl.restype is not None
assert crypt_facade._use_crypt_r is False
assert crypt_facade._crypt_gensalt_impl is not None
assert crypt_facade._crypt_gensalt_impl.argtypes is not None
assert crypt_facade._crypt_gensalt_impl.restype is not None
assert crypt_facade._use_crypt_gensalt_rn is False
assert crypt_facade.has_crypt_gensalt
def test_crypt_fail_errno(self, mocker: MockerFixture) -> None:
"""Test crypt() setting failure errno raises OSError."""
mocker.patch('ctypes.get_errno', return_value=errno.EBADFD)
crypt_facade = CryptFacade()
with pytest.raises(OSError, match=r'crypt failed:'):
crypt_facade.crypt(b"test", b"123")
def test_crypt_result_none(self, mocker: MockerFixture) -> None:
"""Test crypt() implementation returning None raises ValueError."""
crypt_facade = CryptFacade()
mocker.patch.object(crypt_facade, '_crypt_impl', return_value=None)
with pytest.raises(ValueError, match=r'crypt failed: invalid salt or unsupported algorithm'):
crypt_facade.crypt(b"test", b"123")
def test_crypt_result_failure(self, mocker: MockerFixture) -> None:
"""Test crypt() implementation returning failure token raises ValueError."""
crypt_facade = CryptFacade()
mocker.patch.object(crypt_facade, '_crypt_impl', return_value=list(_FAILURE_TOKENS)[0])
with pytest.raises(ValueError, match=r'crypt failed: invalid salt or unsupported algorithm'):
crypt_facade.crypt(b"test", b"123")
def test_crypt_gensalt_called_with_no_impl(self, mocker: MockerFixture) -> None:
"""Calling crypt_gensalt() without impl should raise NotImplementedError."""
crypt_facade = CryptFacade()
mock_prop = mocker.patch('ansible._internal._encryption._crypt.CryptFacade.has_crypt_gensalt', new_callable=mocker.PropertyMock)
mock_prop.return_value = False
with pytest.raises(NotImplementedError, match=r'crypt_gensalt not available \(requires libxcrypt\)'):
crypt_facade.crypt_gensalt(b"", 1, b"")
def test_crypt_gensalt(self, mocker: MockerFixture) -> None:
"""Test the NOT _use_crypt_gensalt_rn code path of crypt_gensalt()."""
crypt_facade = CryptFacade()
crypt_facade._use_crypt_gensalt_rn = False
mock_impl = mocker.patch.object(crypt_facade, '_crypt_gensalt_impl', return_value='')
crypt_facade.crypt_gensalt(b'', 1, b'')
mock_impl.assert_called_once_with(b'', 1, b'', 0)
def test_crypt_gensalt_fail_errno(self, mocker: MockerFixture) -> None:
"""Test crypt_gensalt() setting failure errno raises OSError."""
mocker.patch('ctypes.get_errno', return_value=errno.EBADFD)
crypt_facade = CryptFacade()
with pytest.raises(OSError, match=r'crypt_gensalt failed:'):
crypt_facade.crypt_gensalt(b'', 1, b'')
def test_crypt_gensalt_result_none(self, mocker: MockerFixture) -> None:
"""Test crypt_gensalt() implementation returning None raises ValueError."""
crypt_facade = CryptFacade()
mocker.patch.object(crypt_facade, '_crypt_gensalt_impl', return_value=None)
with pytest.raises(ValueError, match=r'crypt_gensalt failed: unable to generate salt'):
crypt_facade.crypt_gensalt(b'', 1, b'')
def test_crypt_gensalt_result_failure(self, mocker: MockerFixture) -> None:
"""Test crypt_gensalt() implementation returning failure token raises ValueError."""
crypt_facade = CryptFacade()
# Skip the _rn version as it modifies impl return value
crypt_facade._use_crypt_gensalt_rn = False
mocker.patch.object(crypt_facade, '_crypt_gensalt_impl', return_value=list(_FAILURE_TOKENS)[0])
with pytest.raises(ValueError, match=r'crypt_gensalt failed: invalid prefix or unsupported algorithm'):
crypt_facade.crypt_gensalt(b'', 1, b'')