aboutsummaryrefslogtreecommitdiffstats
path: root/tests/test_redact.py
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-08 13:29:48 +0200
committerDanilo M. <danix@danix.xyz>2026-09-08 13:29:48 +0200
commit3126606d5e761e151fee0caab34d2cca2b9b3ee7 (patch)
tree80d014414c85843e5055a3923980c6847e2339e3 /tests/test_redact.py
parent5caedfb5068683ef793a338fae30f8269f9f865d (diff)
downloadabusectl-3126606d5e761e151fee0caab34d2cca2b9b3ee7.tar.gz
abusectl-3126606d5e761e151fee0caab34d2cca2b9b3ee7.zip
feat: redact recipient identifiers inside URLs
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KphFXTc2QajxXsHWyvGJ4R
Diffstat (limited to 'tests/test_redact.py')
-rw-r--r--tests/test_redact.py106
1 files changed, 106 insertions, 0 deletions
diff --git a/tests/test_redact.py b/tests/test_redact.py
new file mode 100644
index 0000000..91ab291
--- /dev/null
+++ b/tests/test_redact.py
@@ -0,0 +1,106 @@
+# Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+"""Tests for abusectl.redact: query-value redaction and redirect recovery."""
+
+import unittest
+
+from abusectl import redact
+
+
+class TestRedactUrl(unittest.TestCase):
+ def test_query_values_are_redacted_and_names_kept(self):
+ # The names fingerprint the kit; the values identify the recipient.
+ self.assertEqual(
+ redact.url("http://login.example.invalid/verify?id=abc&src=mail"),
+ "http://login.example.invalid/verify?id=REDACTED&src=REDACTED",
+ )
+
+ def test_a_url_with_no_query_is_unchanged(self):
+ self.assertEqual(
+ redact.url("http://login.example.invalid/verify"),
+ "http://login.example.invalid/verify",
+ )
+
+ def test_scheme_host_and_path_survive(self):
+ self.assertEqual(
+ redact.url("https://a.example.invalid/one/two/three?x=1"),
+ "https://a.example.invalid/one/two/three?x=REDACTED",
+ )
+
+ def test_a_valueless_parameter_keeps_its_shape(self):
+ self.assertEqual(
+ redact.url("http://a.example.invalid/p?flag"),
+ "http://a.example.invalid/p?flag=REDACTED",
+ )
+
+ def test_repeated_parameter_names_are_all_redacted(self):
+ self.assertEqual(
+ redact.url("http://a.example.invalid/p?t=1&t=2"),
+ "http://a.example.invalid/p?t=REDACTED&t=REDACTED",
+ )
+
+
+class TestSuspectPathSegments(unittest.TestCase):
+ def test_a_base64_looking_segment_is_flagged(self):
+ # Flagged for review, NOT redacted: a path may be meaningful.
+ found = redact.suspect_path_segments(
+ "http://a.example.invalid/verify/dGVzdEBleGFtcGxlLm9yZw/"
+ )
+ self.assertEqual(found, ["dGVzdEBleGFtcGxlLm9yZw"])
+
+ def test_a_long_hex_segment_is_flagged(self):
+ found = redact.suspect_path_segments(
+ "http://a.example.invalid/c/5f4dcc3b5aa765d61d8327deb882cf99"
+ )
+ self.assertEqual(found, ["5f4dcc3b5aa765d61d8327deb882cf99"])
+
+ def test_ordinary_path_words_are_not_flagged(self):
+ found = redact.suspect_path_segments(
+ "http://a.example.invalid/account/verify/now"
+ )
+ self.assertEqual(found, [])
+
+ def test_a_short_segment_is_not_flagged(self):
+ # "news" is base64-shaped and four characters. Too short to carry an
+ # address, and flagging it would train the user to ignore the flag.
+ found = redact.suspect_path_segments("http://a.example.invalid/news")
+ self.assertEqual(found, [])
+
+
+class TestUrlValuedParameters(unittest.TestCase):
+ def test_a_redirect_target_is_recovered(self):
+ found = redact.url_valued_parameters(
+ "http://t.example.invalid/c?url=http%3A%2F%2Fevil.example.invalid%2Fp"
+ )
+ self.assertEqual(found, ["http://evil.example.invalid/p"])
+
+ def test_a_tracking_token_is_not_mistaken_for_one(self):
+ found = redact.url_valued_parameters(
+ "http://t.example.invalid/c?u=dGVzdEBleGFtcGxlLm9yZw"
+ )
+ self.assertEqual(found, [])
+
+ def test_the_original_is_still_fully_redacted(self):
+ # Recovery does not loosen the rule: the redirector itself keeps every
+ # value blanked, including the one the target was recovered from.
+ raw = "http://t.example.invalid/c?url=http%3A%2F%2Fe.example.invalid%2Fp&u=tok"
+ self.assertEqual(
+ redact.url(raw),
+ "http://t.example.invalid/c?url=REDACTED&u=REDACTED",
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()