# Copyright (C) 2026 Danilo M. # # 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", ) class TestLeakResistance(unittest.TestCase): def test_a_fragment_key_value_pair_is_redacted(self): out = redact.url("http://a.invalid/p?x=1#e=victim@example.org") self.assertNotIn("victim@example.org", out) def test_userinfo_is_stripped_not_redacted_in_place(self): out = redact.url("http://victim%40example.org:pw@a.invalid/p") self.assertNotIn("victim%40example.org", out) self.assertNotIn("pw", out) def test_a_query_hidden_inside_a_path_segment_is_flagged(self): found = redact.suspect_path_segments( "http://a.invalid/p%3Fe=victim@example.org" ) self.assertNotEqual(found, []) def test_no_recipient_marker_survives_any_placement(self): # The same address, placed everywhere a URL can hide one. marker = "victim@example.org" encoded = "victim%40example.org" for raw in ( f"http://a.invalid/p?e={marker}", f"http://a.invalid/p?x=1#e={marker}", f"http://{encoded}:pw@a.invalid/p", f"http://a.invalid/p?a=1&b=2#{marker}", ): with self.subTest(raw=raw): out = redact.url(raw) self.assertNotIn(marker, out) self.assertNotIn(encoded, out) class TestHasUserinfo(unittest.TestCase): def test_userinfo_present_is_reported(self): self.assertTrue( redact.has_userinfo("http://victim%40example.org:pw@a.invalid/p") ) def test_no_userinfo_is_reported_absent(self): self.assertFalse(redact.has_userinfo("http://a.invalid/p")) if __name__ == "__main__": unittest.main()