# 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 the RDAP protocol module.""" import email import unittest import urllib.error import urllib.request from abusectl import rdap class RedirectPolicy(unittest.TestCase): """A redirect is remote data directing our next request. urllib.request.Request is used rather than a hand-rolled fake, because HTTPRedirectHandler reads attributes (origin_req_host, unverifiable, timeout) that a fake would have to reproduce exactly to prove anything. """ def _request(self): return urllib.request.Request("https://rdap.example.invalid/ip/192.0.2.1") def test_an_https_to_http_downgrade_is_refused(self): handler = rdap._NoDowngradeRedirectHandler() with self.assertRaises(urllib.error.HTTPError): handler.redirect_request( self._request(), None, 302, "Found", email.message_from_string(""), "http://rdap.example.invalid/ip/192.0.2.1", ) def test_an_https_to_https_redirect_is_allowed(self): handler = rdap._NoDowngradeRedirectHandler() result = handler.redirect_request( self._request(), None, 302, "Found", email.message_from_string(""), "https://other.example.invalid/ip/192.0.2.1", ) self.assertIsNotNone(result) import json import tempfile import time from pathlib import Path class Bootstrap(unittest.TestCase): def setUp(self): self.tmp = tempfile.TemporaryDirectory() self.cache = Path(self.tmp.name) self.addCleanup(self.tmp.cleanup) def test_a_missing_file_is_fetched_and_cached(self): calls = [] def fetch(url): calls.append(url) return {"services": []} data = rdap.bootstrap("ipv4", cache_root=self.cache, fetch=fetch) self.assertEqual(data, {"services": []}) self.assertEqual(calls, ["https://data.iana.org/rdap/ipv4.json"]) self.assertTrue((self.cache / "ipv4.json").exists()) def test_a_fresh_cache_is_not_refetched(self): (self.cache / "ipv4.json").write_text(json.dumps({"services": ["cached"]})) def fetch(url): raise AssertionError(f"should not have fetched {url}") data = rdap.bootstrap("ipv4", cache_root=self.cache, fetch=fetch) self.assertEqual(data, {"services": ["cached"]}) def test_a_stale_cache_is_refetched(self): path = self.cache / "ipv4.json" path.write_text(json.dumps({"services": ["old"]})) old = time.time() - (rdap._BOOTSTRAP_TTL + 60) import os os.utime(path, (old, old)) data = rdap.bootstrap( "ipv4", cache_root=self.cache, fetch=lambda url: {"services": ["new"]} ) self.assertEqual(data, {"services": ["new"]}) def test_a_failed_refetch_falls_back_to_the_stale_copy(self): """Losing IANA must not stop the user filing a report. Last week's map is almost certainly still correct, and a stale bootstrap fails safe: the worst case is querying a server that has moved, which misses and reads as no contact. """ path = self.cache / "ipv4.json" path.write_text(json.dumps({"services": ["old"]})) old = time.time() - (rdap._BOOTSTRAP_TTL + 60) import os os.utime(path, (old, old)) def fetch(url): raise OSError("network is unreachable") data = rdap.bootstrap("ipv4", cache_root=self.cache, fetch=fetch) self.assertEqual(data, {"services": ["old"]}) def test_a_failed_fetch_with_no_cache_raises(self): def fetch(url): raise OSError("network is unreachable") with self.assertRaises(rdap.BootstrapUnavailable): rdap.bootstrap("ipv4", cache_root=self.cache, fetch=fetch) if __name__ == "__main__": unittest.main()