|
1 | 1 | # This file is part of cloud-init. See LICENSE file for license information. |
2 | 2 | # pylint: disable=attribute-defined-outside-init |
3 | 3 |
|
| 4 | +import builtins |
4 | 5 | import copy |
5 | 6 | import datetime |
6 | 7 | import json |
7 | 8 | import logging |
8 | 9 | import os |
9 | 10 | import stat |
| 11 | +import sys |
10 | 12 | import xml.etree.ElementTree as ET |
11 | 13 | from pathlib import Path |
12 | 14 |
|
13 | | -import passlib.hash |
| 15 | +try: |
| 16 | + import passlib.hash |
| 17 | +except ImportError: |
| 18 | + passlib = None # type: ignore |
14 | 19 | import pytest |
15 | 20 | import requests |
16 | 21 |
|
@@ -1732,12 +1737,13 @@ def test_username_used(self, get_ds): |
1732 | 1737 |
|
1733 | 1738 | assert "ssh_pwauth" not in dsrc.cfg |
1734 | 1739 |
|
| 1740 | + @pytest.mark.skipif(passlib is None, reason="passlib not installed") |
1735 | 1741 | def test_password_given(self, get_ds, mocker): |
1736 | 1742 | # The crypt module has platform-specific behavior and the purpose of |
1737 | 1743 | # this test isn't to verify the differences between crypt and passlib, |
1738 | 1744 | # so hardcode passlib usage as crypt is deprecated. |
1739 | 1745 | mocker.patch.object( |
1740 | | - dsaz, "blowfish_hash", passlib.hash.sha512_crypt.hash |
| 1746 | + dsaz, "hash_password", passlib.hash.sha512_crypt.hash |
1741 | 1747 | ) |
1742 | 1748 | data = { |
1743 | 1749 | "ovfcontent": construct_ovf_env( |
@@ -2434,6 +2440,19 @@ def test_wb_invalid_ovf_env_xml_calls_read_azure_ovf(self, tmp_path): |
2434 | 2440 | == cm.value.reason |
2435 | 2441 | ) |
2436 | 2442 |
|
| 2443 | + def test_import_error_from_failed_import(self): |
| 2444 | + """Attempt to import a module that is not present""" |
| 2445 | + try: |
| 2446 | + import nonexistent_module_that_will_never_exist # type: ignore[import-not-found] # noqa: F401 # isort:skip |
| 2447 | + except ImportError as error: |
| 2448 | + reportable_error = errors.ReportableErrorImportError(error=error) |
| 2449 | + |
| 2450 | + assert ( |
| 2451 | + reportable_error.reason == "error importing " |
| 2452 | + "nonexistent_module_that_will_never_exist library" |
| 2453 | + ) |
| 2454 | + assert reportable_error.supporting_data["error"] == repr(error) |
| 2455 | + |
2437 | 2456 |
|
2438 | 2457 | class TestReadAzureOvf: |
2439 | 2458 | def test_invalid_xml_raises_non_azure_ds(self): |
@@ -5837,14 +5856,6 @@ def test_missing_secondary( |
5837 | 5856 | assert azure_ds.validate_imds_network_metadata(imds_md) is False |
5838 | 5857 |
|
5839 | 5858 |
|
5840 | | -class TestDependencyFallback: |
5841 | | - def test_dependency_fallback(self): |
5842 | | - """Ensure that crypt/passlib import failover gets exercised on all |
5843 | | - Python versions |
5844 | | - """ |
5845 | | - assert dsaz.encrypt_pass("`") |
5846 | | - |
5847 | | - |
5848 | 5859 | class TestQueryVmId: |
5849 | 5860 | @mock.patch.object( |
5850 | 5861 | identity, "query_system_uuid", side_effect=["test-system-uuid"] |
@@ -5907,3 +5918,102 @@ def test_query_vm_id_vm_id_conversion_failure( |
5907 | 5918 |
|
5908 | 5919 | mock_query_system_uuid.assert_called_once() |
5909 | 5920 | mock_convert_uuid.assert_called_once_with("test-system-uuid") |
| 5921 | + |
| 5922 | + |
| 5923 | +class TestHashPassword: |
| 5924 | + """Tests for the hash_password function.""" |
| 5925 | + |
| 5926 | + def test_dependency_fallback(self): |
| 5927 | + """Ensure that crypt/passlib import failover gets exercised on all |
| 5928 | + Python versions |
| 5929 | + """ |
| 5930 | + result = dsaz.hash_password("`") |
| 5931 | + assert result |
| 5932 | + assert result.startswith("$6$") |
| 5933 | + |
| 5934 | + def test_crypt_working(self): |
| 5935 | + """Test that hash_password uses crypt when available.""" |
| 5936 | + mock_crypt = mock.MagicMock() |
| 5937 | + mock_crypt.METHOD_SHA512 = "sha512" |
| 5938 | + mock_crypt.mksalt.return_value = "$6$saltvalue" |
| 5939 | + mock_crypt.crypt.return_value = "$6$saltvalue$hashedpassword" |
| 5940 | + |
| 5941 | + with mock.patch.dict("sys.modules", {"crypt": mock_crypt}): |
| 5942 | + result = dsaz.hash_password("testpassword") |
| 5943 | + |
| 5944 | + mock_crypt.mksalt.assert_called_once_with("sha512") |
| 5945 | + mock_crypt.crypt.assert_called_once_with( |
| 5946 | + "testpassword", "$6$saltvalue" |
| 5947 | + ) |
| 5948 | + assert result == "$6$saltvalue$hashedpassword" |
| 5949 | + |
| 5950 | + def test_crypt_not_installed_passlib_fallback(self): |
| 5951 | + """Test that hash_password falls back to passlib when missing crypt.""" |
| 5952 | + real_import = builtins.__import__ |
| 5953 | + passlib_available = True |
| 5954 | + try: |
| 5955 | + import passlib.hash as _passlib_hash |
| 5956 | + except ImportError: |
| 5957 | + passlib_available = False |
| 5958 | + |
| 5959 | + if passlib_available: |
| 5960 | + # passlib is installed; block crypt and let passlib work normally |
| 5961 | + def mock_import(name, *args, **kwargs): |
| 5962 | + if name == "crypt": |
| 5963 | + raise ImportError("No module named 'crypt'") |
| 5964 | + return real_import(name, *args, **kwargs) |
| 5965 | + |
| 5966 | + with mock.patch.object( |
| 5967 | + builtins, "__import__", side_effect=mock_import |
| 5968 | + ): |
| 5969 | + result = dsaz.hash_password("testpassword") |
| 5970 | + |
| 5971 | + # Verify we got a valid SHA-512 hash from passlib |
| 5972 | + assert result.startswith("$6$") |
| 5973 | + assert _passlib_hash.sha512_crypt.verify("testpassword", result) |
| 5974 | + else: |
| 5975 | + # passlib is not installed; mock it to return a known hash |
| 5976 | + mock_passlib_hash = mock.MagicMock() |
| 5977 | + mock_passlib_hash.sha512_crypt.hash.return_value = ( |
| 5978 | + "$6$mocksalt$mockedhash" |
| 5979 | + ) |
| 5980 | + |
| 5981 | + def mock_import(name, *args, **kwargs): |
| 5982 | + if name == "crypt": |
| 5983 | + raise ImportError("No module named 'crypt'") |
| 5984 | + if name == "passlib.hash": |
| 5985 | + mod = mock.MagicMock() |
| 5986 | + mod.hash = mock_passlib_hash |
| 5987 | + sys.modules["passlib"] = mod |
| 5988 | + sys.modules["passlib.hash"] = mock_passlib_hash |
| 5989 | + return mod |
| 5990 | + return real_import(name, *args, **kwargs) |
| 5991 | + |
| 5992 | + with mock.patch.object( |
| 5993 | + builtins, "__import__", side_effect=mock_import |
| 5994 | + ): |
| 5995 | + result = dsaz.hash_password("testpassword") |
| 5996 | + |
| 5997 | + assert result == "$6$mocksalt$mockedhash" |
| 5998 | + mock_passlib_hash.sha512_crypt.hash.assert_called_once_with( |
| 5999 | + "testpassword" |
| 6000 | + ) |
| 6001 | + |
| 6002 | + def test_crypt_and_passlib_unavailable_raises_error(self): |
| 6003 | + """Test that hash_password raises ReportableErrorImportError.""" |
| 6004 | + real_import = builtins.__import__ |
| 6005 | + |
| 6006 | + def mock_import(name, *args, **kwargs): |
| 6007 | + if name == "crypt": |
| 6008 | + raise ImportError("No module named 'crypt'") |
| 6009 | + if name == "passlib.hash": |
| 6010 | + raise ImportError("No module named 'passlib'", name="passlib") |
| 6011 | + return real_import(name, *args, **kwargs) |
| 6012 | + |
| 6013 | + with mock.patch.object( |
| 6014 | + builtins, "__import__", side_effect=mock_import |
| 6015 | + ): |
| 6016 | + with pytest.raises(errors.ReportableErrorImportError) as exc_info: |
| 6017 | + dsaz.hash_password("testpassword") |
| 6018 | + |
| 6019 | + assert "passlib" in exc_info.value.reason |
0 commit comments