aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/test_config.py170
-rw-r--r--tests/test_report.py26
2 files changed, 195 insertions, 1 deletions
diff --git a/tests/test_config.py b/tests/test_config.py
index 6fa8619..1815993 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -20,7 +20,25 @@ import tempfile
import unittest
from unittest import mock
-from abusectl import config
+from abusectl import config, report
+
+# Minimal, kept local rather than imported from test_report: these tests are
+# about the shape config produces, and they must not start failing because a
+# report fixture grew a field.
+_MANIFEST = {
+ "format": 1,
+ "case_id": "2026-09-07-aaaa",
+ "iocs": [
+ {"id": "ioc-1", "type": "ipv4", "value": "203.0.113.42",
+ "origin": "received-chain", "confidence": "boundary-hop"},
+ ],
+ "headers": [("From", '"Example Bank" <phish@sender.invalid>')],
+ "contacts": [
+ {"iocs": ["ioc-1"], "query": "203.0.113.42",
+ "abuse": ["abuse@host.invalid"], "source": "rdap"},
+ ],
+}
+_DESTINATION = {"id": "d1", "target": "abuse@host.invalid", "iocs": ["ioc-1"]}
class TestConfig(unittest.TestCase):
@@ -105,6 +123,156 @@ class TestConfig(unittest.TestCase):
with self.assertRaises(ValueError):
config.load(path)
+ def test_the_reporter_identity_is_read(self):
+ path = self._write(
+ "[general]\n"
+ 'trusted_relays = ["192.0.2.0/24"]\n'
+ "\n"
+ "[reporter]\n"
+ 'name = "A Reporter"\n'
+ 'org = "Example Consulting"\n'
+ 'email = "reporter@example.org"\n'
+ )
+ loaded = config.load(path)
+ self.assertEqual(loaded.reporter["name"], "A Reporter")
+ self.assertEqual(loaded.reporter["org"], "Example Consulting")
+ self.assertEqual(loaded.reporter["email"], "reporter@example.org")
+
+ def test_an_absent_reporter_section_is_an_empty_dict_not_a_crash(self):
+ path = self._write('[general]\ntrusted_relays = ["192.0.2.0/24"]\n')
+ self.assertEqual(config.load(path).reporter, {})
+
+ def test_an_empty_value_is_treated_as_absent(self):
+ # Same rule as cases and as every key init writes: skipped is ABSENT,
+ # never "". An empty org must not reach a report as a stray comma.
+ path = self._write(
+ "[general]\n"
+ 'trusted_relays = ["192.0.2.0/24"]\n'
+ "\n"
+ "[reporter]\n"
+ 'name = "A Reporter"\n'
+ 'org = ""\n'
+ 'email = "reporter@example.org"\n'
+ )
+ reporter = config.load(path).reporter
+ self.assertNotIn("org", reporter)
+ self.assertEqual(reporter["name"], "A Reporter")
+
+ def test_a_whitespace_only_value_is_treated_as_absent(self):
+ path = self._write(
+ "[general]\n"
+ 'trusted_relays = ["192.0.2.0/24"]\n'
+ "\n"
+ "[reporter]\n"
+ 'org = " "\n'
+ 'email = "reporter@example.org"\n'
+ )
+ self.assertNotIn("org", config.load(path).reporter)
+
+ def test_a_reporter_value_is_stored_stripped(self):
+ # The name becomes a From display name. Leading and trailing space
+ # survives into the header verbatim, which is sloppy at best and
+ # affects folding at worst.
+ path = self._write(
+ "[general]\n"
+ 'trusted_relays = ["192.0.2.0/24"]\n'
+ "\n"
+ "[reporter]\n"
+ 'name = " A Reporter "\n'
+ 'email = " reporter@example.org "\n'
+ )
+ reporter = config.load(path).reporter
+ self.assertEqual(reporter["name"], "A Reporter")
+ self.assertEqual(reporter["email"], "reporter@example.org")
+
+ def test_a_reporter_value_that_is_not_a_string_is_rejected_clearly(self):
+ # Not dropped. Dropping reads as not-configured, and this is the one
+ # identity the tool discloses deliberately: a typo that silently
+ # removes the reply address must be reported against the file that
+ # holds it, the same way a non-string trusted_relays entry is.
+ path = self._write(
+ "[general]\n"
+ 'trusted_relays = ["192.0.2.0/24"]\n'
+ "\n"
+ "[reporter]\n"
+ "name = 42\n"
+ )
+ with self.assertRaises(ValueError) as caught:
+ config.load(path)
+ self.assertIn("must be a string", str(caught.exception))
+ self.assertIn("name", str(caught.exception))
+
+ def test_a_non_string_email_is_rejected_rather_than_dropped(self):
+ path = self._write(
+ "[general]\n"
+ 'trusted_relays = ["192.0.2.0/24"]\n'
+ "\n"
+ "[reporter]\n"
+ 'email = ["reporter@example.org"]\n'
+ )
+ with self.assertRaises(ValueError):
+ config.load(path)
+
+ def test_a_string_reporter_section_is_rejected_clearly(self):
+ # reporter = "A Reporter" is valid TOML and would otherwise raise
+ # AttributeError naming nothing the user can find in their file. It
+ # has to precede [general]: a bare key written after a table header
+ # belongs to that table, not to the document.
+ path = self._write(
+ 'reporter = "me"\n\n[general]\ntrusted_relays = ["192.0.2.0/24"]\n'
+ )
+ with self.assertRaises(ValueError) as caught:
+ config.load(path)
+ self.assertIn("must be a table", str(caught.exception))
+
+ def test_an_unknown_reporter_key_is_ignored(self):
+ # Ignored rather than refused: unlike a manifest format, an unknown
+ # key here loses nothing. Keeping only the three the spec names is
+ # what stops it reaching a report body.
+ path = self._write(
+ "[general]\n"
+ 'trusted_relays = ["192.0.2.0/24"]\n'
+ "\n"
+ "[reporter]\n"
+ 'email = "reporter@example.org"\n'
+ 'phone = "+1 555 0100"\n'
+ )
+ reporter = config.load(path).reporter
+ self.assertNotIn("phone", reporter)
+ self.assertEqual(reporter["email"], "reporter@example.org")
+
+ def test_a_configured_identity_builds_a_report_body(self):
+ # The round trip that only shows up much later otherwise: the dict
+ # config produces must be the shape report.build() consumes.
+ path = self._write(
+ "[general]\n"
+ 'trusted_relays = ["192.0.2.0/24"]\n'
+ "\n"
+ "[reporter]\n"
+ 'name = "A Reporter"\n'
+ 'org = "Example Consulting"\n'
+ 'email = "reporter@example.org"\n'
+ )
+ body = report.build(_MANIFEST, _DESTINATION, config.load(path).reporter)
+ self.assertIn("A Reporter <reporter@example.org>", body)
+ self.assertIn(
+ "Reported by: A Reporter, Example Consulting "
+ "<reporter@example.org>", body)
+
+ def test_an_identity_with_only_an_email_still_builds_a_body(self):
+ # Every key is individually skippable per the spec, so config drops
+ # the skipped ones and build() must survive their absence rather
+ # than raising KeyError on a case that parsed fine.
+ path = self._write(
+ "[general]\n"
+ 'trusted_relays = ["192.0.2.0/24"]\n'
+ "\n"
+ "[reporter]\n"
+ 'email = "reporter@example.org"\n'
+ )
+ body = report.build(_MANIFEST, _DESTINATION, config.load(path).reporter)
+ self.assertIn("reporter@example.org", body)
+
def test_the_config_path_follows_xdg_config_home(self):
with mock.patch.dict(os.environ, {"XDG_CONFIG_HOME": "/tmp/xdg-probe"}):
self.assertEqual(
diff --git a/tests/test_report.py b/tests/test_report.py
index e87956f..9159dc7 100644
--- a/tests/test_report.py
+++ b/tests/test_report.py
@@ -640,6 +640,32 @@ class TextPart(unittest.TestCase):
self.assertLessEqual(len(line), 72, line)
self.assertIn(long_subject, report.unwrap(text))
+ def test_an_identity_missing_a_name_or_org_still_builds(self):
+ """Each [reporter] key is individually skippable.
+
+ config drops a skipped one rather than storing "", so a partial
+ identity is the normal shape here, not a malformed one. Subscripting
+ it raised KeyError on a case that had parsed perfectly.
+ """
+ destination = report.email_destinations(MANIFEST["contacts"])[0]
+
+ text = report.text_part(MANIFEST, destination,
+ {"email": "reporter@example.org"})
+ self.assertIn("Reported by: <reporter@example.org>", text)
+ self.assertNotIn(",", report.unwrap(text).split("Reported by:")[1]
+ .splitlines()[0])
+
+ text = report.text_part(MANIFEST, destination,
+ {"name": "A Reporter",
+ "email": "reporter@example.org"})
+ self.assertIn("Reported by: A Reporter <reporter@example.org>", text)
+
+ text = report.text_part(MANIFEST, destination,
+ {"org": "Example Consulting",
+ "email": "reporter@example.org"})
+ self.assertIn("Reported by: Example Consulting "
+ "<reporter@example.org>", text)
+
def test_a_long_reporter_identity_is_not_truncated(self):
"""The identity is the one thing disclosed deliberately.