diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test_init.py | 139 |
1 files changed, 138 insertions, 1 deletions
diff --git a/tests/test_init.py b/tests/test_init.py index 798ed26..2e2ebd5 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -14,13 +14,15 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """Tests for the first-run config builder in abusectl.init.""" +import contextlib +import io import ipaddress import pathlib import tempfile import tomllib import unittest -from abusectl import config, init +from abusectl import cli, config, init class TestBuildConfig(unittest.TestCase): @@ -363,6 +365,141 @@ class BuildsDestinationSections(unittest.TestCase): {"misp", "abusedb", "urlhaus"}) +class DropsSkippedSections(unittest.TestCase): + """A section the user was ASKED about and skipped must not survive. + + Sections this run does not render are preserved verbatim, which is what + stops an init setting only the relays from discarding a key set + earlier. That rule and a deliberate skip collide: declining MISP at the + prompt left the previous instance in the file, and report would then + submit to an instance the user had just said no to. `drop` is how write + tells the two apart. + """ + + ANSWERS = {"trusted_relays": ["192.0.2.0/24"]} + + def _existing(self, tmp): + path = pathlib.Path(tmp) / "config.toml" + init.write(path, {**self.ANSWERS, + "misp_url": "https://old.example.invalid", + "misp_api_key": "old", + "abusedb_api_key": "olda"}) + return path + + def test_a_dropped_section_is_removed(self): + with tempfile.TemporaryDirectory() as tmp: + path = self._existing(tmp) + init.write(path, self.ANSWERS, force=True, drop=frozenset({"misp"})) + data = tomllib.loads(path.read_text()) + self.assertNotIn("misp", data) + + def test_dropping_one_leaves_the_others(self): + with tempfile.TemporaryDirectory() as tmp: + path = self._existing(tmp) + init.write(path, self.ANSWERS, force=True, drop=frozenset({"misp"})) + data = tomllib.loads(path.read_text()) + self.assertEqual(data["abusedb"]["api_key"], "olda") + + def test_without_drop_a_section_is_still_preserved(self): + # The rule drop exists to qualify, not to replace. A non-interactive + # caller passes no drop and must behave exactly as before. + with tempfile.TemporaryDirectory() as tmp: + path = self._existing(tmp) + init.write(path, self.ANSWERS, force=True) + data = tomllib.loads(path.read_text()) + self.assertEqual(data["misp"]["api_key"], "old") + + def test_a_dropped_section_that_is_answered_is_still_written(self): + # drop names what was skipped. An answered section renders normally + # and must not be removed by a stale drop entry. + with tempfile.TemporaryDirectory() as tmp: + path = self._existing(tmp) + init.write(path, {**self.ANSWERS, + "misp_url": "https://new.example.invalid", + "misp_api_key": "new"}, + force=True, drop=frozenset({"misp"})) + data = tomllib.loads(path.read_text()) + self.assertEqual(data["misp"]["url"], "https://new.example.invalid") + + def test_the_result_still_parses_and_loads(self): + with tempfile.TemporaryDirectory() as tmp: + path = self._existing(tmp) + init.write(path, self.ANSWERS, force=True, + drop=frozenset({"misp", "abusedb"})) + self.assertEqual(config.load(path).destinations, set()) + + +class AsksBeforeKeepingASkippedSection(unittest.TestCase): + """The prompt half, driven directly rather than through stdin.""" + + SKIPPED = {"misp_url": "", "misp_api_key": "", "abusedb_api_key": ""} + + def _answer(self, replies): + replies = iter(replies) + cli._ask = lambda question: next(replies) + + def setUp(self): + self._real_ask = cli._ask + + def tearDown(self): + cli._ask = self._real_ask + + def test_declining_drops_the_section(self): + with tempfile.TemporaryDirectory() as tmp: + path = pathlib.Path(tmp) / "config.toml" + init.write(path, {"trusted_relays": ["192.0.2.0/24"], + "misp_url": "https://old.example.invalid", + "misp_api_key": "old"}) + self._answer(["n"]) + with contextlib.redirect_stdout(io.StringIO()): + dropped = cli._confirm_dropped(path, self.SKIPPED) + self.assertEqual(dropped, frozenset({"misp"})) + + def test_the_default_keeps_it(self): + # Enter means keep: removal is the destructive answer and must be + # typed, not fallen into. + with tempfile.TemporaryDirectory() as tmp: + path = pathlib.Path(tmp) / "config.toml" + init.write(path, {"trusted_relays": ["192.0.2.0/24"], + "misp_url": "https://old.example.invalid", + "misp_api_key": "old"}) + self._answer([""]) + with contextlib.redirect_stdout(io.StringIO()): + dropped = cli._confirm_dropped(path, self.SKIPPED) + self.assertEqual(dropped, frozenset()) + + def test_an_answered_section_is_never_asked_about(self): + with tempfile.TemporaryDirectory() as tmp: + path = pathlib.Path(tmp) / "config.toml" + init.write(path, {"trusted_relays": ["192.0.2.0/24"], + "misp_url": "https://old.example.invalid", + "misp_api_key": "old"}) + # No replies queued: asking anything raises StopIteration. + self._answer([]) + with contextlib.redirect_stdout(io.StringIO()): + dropped = cli._confirm_dropped( + path, {"misp_url": "https://new.example.invalid", + "misp_api_key": "new"}) + self.assertEqual(dropped, frozenset()) + + def test_a_first_run_asks_nothing(self): + with tempfile.TemporaryDirectory() as tmp: + self._answer([]) + dropped = cli._confirm_dropped( + pathlib.Path(tmp) / "absent.toml", self.SKIPPED) + self.assertEqual(dropped, frozenset()) + + def test_an_unreadable_config_asks_nothing(self): + # Not a file to make removal decisions from, and write() backs it + # up regardless. + with tempfile.TemporaryDirectory() as tmp: + path = pathlib.Path(tmp) / "config.toml" + path.write_text("not [valid toml", encoding="utf-8") + self._answer([]) + dropped = cli._confirm_dropped(path, self.SKIPPED) + self.assertEqual(dropped, frozenset()) + + if __name__ == "__main__": unittest.main() |
