aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/test_init.py139
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()