|
| 1 | +import dataclasses |
| 2 | +from copy import deepcopy |
| 3 | +from pathlib import Path |
| 4 | +from unittest.mock import patch |
| 5 | + |
| 6 | +import pytest |
| 7 | +from tomlkit import TOMLDocument, dumps, parse |
| 8 | +from twyn.base.constants import DEFAULT_PROJECT_TOML_FILE, DEFAULT_TOP_PYPI_PACKAGES, AvailableLoggingLevels |
| 9 | +from twyn.config.config_handler import ConfigHandler, ReadTwynConfiguration, TwynConfiguration |
| 10 | +from twyn.config.exceptions import ( |
| 11 | + AllowlistPackageAlreadyExistsError, |
| 12 | + AllowlistPackageDoesNotExistError, |
| 13 | + InvalidSelectorMethodError, |
| 14 | + TOMLError, |
| 15 | +) |
| 16 | +from twyn.file_handler.exceptions import PathNotFoundError |
| 17 | +from twyn.file_handler.file_handler import FileHandler |
| 18 | + |
| 19 | + |
| 20 | +class TestConfig: |
| 21 | + def throw_exception(self): |
| 22 | + raise PathNotFoundError |
| 23 | + |
| 24 | + def test_config_raises_for_unknown_file(self): |
| 25 | + with pytest.raises(TOMLError): |
| 26 | + ConfigHandler(FileHandler("non-existent-file.toml")).resolve_config() |
| 27 | + |
| 28 | + def test_read_config_values(self, pyproject_toml_file): |
| 29 | + config = ConfigHandler(file_handler=FileHandler(pyproject_toml_file)).resolve_config() |
| 30 | + assert config.dependency_file == "my_file.txt" |
| 31 | + assert config.selector_method == "all" |
| 32 | + assert config.logging_level == AvailableLoggingLevels.debug |
| 33 | + assert config.allowlist == {"boto4", "boto2"} |
| 34 | + |
| 35 | + def test_get_twyn_data_from_file(self, pyproject_toml_file): |
| 36 | + handler = ConfigHandler(FileHandler(str(pyproject_toml_file))) |
| 37 | + |
| 38 | + toml = handler._read_toml() |
| 39 | + twyn_data = ConfigHandler(FileHandler(pyproject_toml_file))._get_read_config(toml) |
| 40 | + assert twyn_data == ReadTwynConfiguration( |
| 41 | + dependency_file="my_file.txt", |
| 42 | + selector_method="all", |
| 43 | + logging_level="debug", |
| 44 | + allowlist={"boto4", "boto2"}, |
| 45 | + package_reference=None, |
| 46 | + ) |
| 47 | + |
| 48 | + def test_write_toml(self, pyproject_toml_file): |
| 49 | + handler = ConfigHandler(FileHandler(pyproject_toml_file)) |
| 50 | + toml = handler._read_toml() |
| 51 | + |
| 52 | + initial_config = handler.resolve_config() |
| 53 | + to_write = deepcopy(initial_config) |
| 54 | + to_write = dataclasses.replace(to_write, allowlist={}) |
| 55 | + |
| 56 | + handler._write_config(toml, to_write) |
| 57 | + |
| 58 | + new_config = handler.resolve_config() |
| 59 | + |
| 60 | + assert new_config != initial_config |
| 61 | + assert new_config.allowlist == set() |
| 62 | + # Writing the config should result in changes in the twyn section but |
| 63 | + # nowhere else |
| 64 | + assert handler._read_toml() == { |
| 65 | + "tool": { |
| 66 | + "poetry": { |
| 67 | + "dependencies": { |
| 68 | + "python": "^3.11", |
| 69 | + "requests": "^2.28.2", |
| 70 | + "dparse": "^0.6.2", |
| 71 | + "click": "^8.1.3", |
| 72 | + "rich": "^13.3.1", |
| 73 | + "rapidfuzz": "^2.13.7", |
| 74 | + "regex": "^2022.10.31", |
| 75 | + }, |
| 76 | + "scripts": {"twyn": "twyn.cli:entry_point"}, |
| 77 | + }, |
| 78 | + "twyn": { |
| 79 | + "dependency_file": "my_file.txt", |
| 80 | + "selector_method": "all", |
| 81 | + "logging_level": "debug", |
| 82 | + "allowlist": {}, |
| 83 | + "package_reference": DEFAULT_TOP_PYPI_PACKAGES, |
| 84 | + }, |
| 85 | + } |
| 86 | + } |
| 87 | + |
| 88 | + |
| 89 | +class TestAllowlistConfigHandler: |
| 90 | + @patch("twyn.file_handler.file_handler.FileHandler.write") |
| 91 | + @patch("twyn.config.config_handler.ConfigHandler._read_toml") |
| 92 | + def test_allowlist_add(self, mock_toml, mock_write_toml): |
| 93 | + mock_toml.return_value = TOMLDocument() |
| 94 | + |
| 95 | + config = ConfigHandler(FileHandler("some-file")) |
| 96 | + |
| 97 | + config.add_package_to_allowlist("mypackage") |
| 98 | + |
| 99 | + final_toml = config._read_toml() |
| 100 | + |
| 101 | + assert final_toml == {"tool": {"twyn": {"allowlist": ["mypackage"]}}} |
| 102 | + assert mock_write_toml.called |
| 103 | + |
| 104 | + @patch("twyn.config.config_handler.ConfigHandler._write_toml") |
| 105 | + @patch("twyn.config.config_handler.ConfigHandler._read_toml") |
| 106 | + def test_allowlist_add_duplicate_error(self, mock_toml, mock_write_toml): |
| 107 | + mock_toml.return_value = parse(dumps({"tool": {"twyn": {"allowlist": ["mypackage"]}}})) |
| 108 | + |
| 109 | + config = ConfigHandler(FileHandler("some-file")) |
| 110 | + with pytest.raises( |
| 111 | + AllowlistPackageAlreadyExistsError, |
| 112 | + match="Package 'mypackage' is already present in the allowlist. Skipping.", |
| 113 | + ): |
| 114 | + config.add_package_to_allowlist("mypackage") |
| 115 | + |
| 116 | + assert not mock_write_toml.called |
| 117 | + |
| 118 | + @patch("twyn.config.config_handler.ConfigHandler._write_toml") |
| 119 | + @patch("twyn.config.config_handler.ConfigHandler._read_toml") |
| 120 | + def test_allowlist_remove_completely(self, mock_toml, mock_write_toml): |
| 121 | + mock_toml.return_value = parse(dumps({"tool": {"twyn": {"allowlist": ["mypackage"]}}})) |
| 122 | + |
| 123 | + config = ConfigHandler(FileHandler("some-file")) |
| 124 | + |
| 125 | + config.remove_package_from_allowlist("mypackage") |
| 126 | + assert config._read_toml() == {"tool": {"twyn": {}}} |
| 127 | + |
| 128 | + @patch("twyn.config.config_handler.ConfigHandler._write_toml") |
| 129 | + @patch("twyn.config.config_handler.ConfigHandler._read_toml") |
| 130 | + def test_allowlist_remove(self, mock_toml, mock_write_toml): |
| 131 | + mock_toml.return_value = parse(dumps({"tool": {"twyn": {"allowlist": ["mypackage", "another-package"]}}})) |
| 132 | + |
| 133 | + config = ConfigHandler(FileHandler("some-file")) |
| 134 | + |
| 135 | + config.remove_package_from_allowlist("mypackage") |
| 136 | + assert config._read_toml() == {"tool": {"twyn": {"allowlist": ["another-package"]}}} |
| 137 | + |
| 138 | + @patch("twyn.config.config_handler.ConfigHandler._write_toml") |
| 139 | + @patch("twyn.config.config_handler.ConfigHandler._read_toml") |
| 140 | + def test_allowlist_remove_non_existent_package_error(self, mock_toml, mock_write_toml): |
| 141 | + mock_toml.return_value = parse(dumps({"tool": {"twyn": {"allowlist": ["mypackage"]}}})) |
| 142 | + |
| 143 | + config = ConfigHandler(FileHandler("some-file")) |
| 144 | + with pytest.raises( |
| 145 | + AllowlistPackageDoesNotExistError, |
| 146 | + match="Package 'mypackage2' is not present in the allowlist. Skipping.", |
| 147 | + ): |
| 148 | + config.remove_package_from_allowlist("mypackage2") |
| 149 | + |
| 150 | + assert not mock_write_toml.called |
| 151 | + |
| 152 | + @pytest.mark.parametrize("valid_selector", ["first-letter", "nearby-letter", "all"]) |
| 153 | + def test_valid_selector_methods_accepted(self, valid_selector: str, tmp_path: Path): |
| 154 | + """Test that all valid selector methods are accepted.""" |
| 155 | + pyproject_toml = tmp_path / "pyproject.toml" |
| 156 | + pyproject_toml.write_text("") |
| 157 | + config = ConfigHandler(FileHandler(str(pyproject_toml))) |
| 158 | + |
| 159 | + # Should not raise any exception |
| 160 | + resolved_config = config.resolve_config(selector_method=valid_selector) |
| 161 | + assert resolved_config.selector_method == valid_selector |
| 162 | + |
| 163 | + def test_invalid_selector_method_rejected(self, tmp_path: Path): |
| 164 | + """Test that invalid selector methods are rejected with appropriate error.""" |
| 165 | + pyproject_toml = tmp_path / "pyproject.toml" |
| 166 | + pyproject_toml.write_text("") |
| 167 | + config = ConfigHandler(FileHandler(str(pyproject_toml))) |
| 168 | + |
| 169 | + with pytest.raises(InvalidSelectorMethodError) as exc_info: |
| 170 | + config.resolve_config(selector_method="random-selector") |
| 171 | + |
| 172 | + error_message = str(exc_info.value) |
| 173 | + assert "Invalid selector_method 'random-selector'" in error_message |
| 174 | + assert "Must be one of: all, first-letter, nearby-letter" in error_message |
| 175 | + |
| 176 | + def test_invalid_selector_method_from_config_file(self, tmp_path: Path): |
| 177 | + """Test that invalid selector method from config file is rejected.""" |
| 178 | + # Create a config file with invalid selector method |
| 179 | + pyproject_toml = tmp_path / "pyproject.toml" |
| 180 | + data = """ |
| 181 | + [tool.twyn] |
| 182 | + selector_method="invalid-selector" |
| 183 | + """ |
| 184 | + pyproject_toml.write_text(data) |
| 185 | + |
| 186 | + config = ConfigHandler(FileHandler(str(pyproject_toml))) |
| 187 | + |
| 188 | + with pytest.raises(InvalidSelectorMethodError) as exc_info: |
| 189 | + config.resolve_config() |
| 190 | + |
| 191 | + error_message = str(exc_info.value) |
| 192 | + assert "Invalid selector_method 'invalid-selector'" in error_message |
| 193 | + assert "Must be one of: all, first-letter, nearby-letter" in error_message |
0 commit comments