# SPDX-FileCopyrightText: 2026 Dennis Fink # # SPDX-License-Identifier: BSD-3-Clause from unittest.mock import Mock, patch import dns.resolver import pytest from webmentions_ssg.url_security import ( AddressResolutionError, is_http_url, is_public_url, ) @pytest.mark.parametrize( ("url", "expected"), [ pytest.param("http://example.com/", True, id="http"), pytest.param("https://example.com/path", True, id="https"), pytest.param("HTTP://EXAMPLE.COM/", True, id="scheme-case-insensitive"), pytest.param("https://example.com:8443/path", True, id="port"), pytest.param("http://[2001:db8::1]/", True, id="ipv6"), pytest.param("ftp://example.com/", False, id="ftp"), pytest.param("mailto:example@example.com", False, id="mailto"), pytest.param("/relative/url", False, id="relative"), pytest.param("//example.com/path", False, id="scheme-relative"), pytest.param("https:///missing-host", False, id="missing-host"), pytest.param("", False, id="empty"), pytest.param("http://[::1", False, id="malformed"), ], ) def test_is_http_url(url: str, expected: bool) -> None: assert is_http_url(url) is expected @patch("webmentions_ssg.url_security.dns.resolver.resolve_name") @pytest.mark.parametrize( ("url", "resolved_addresses"), [ ("http://127.0.0.1/", ["127.0.0.1"]), ("http://127.0.0.1:8080/test", ["127.0.0.1"]), ("http://[::1]/", ["::1"]), ("http://10.0.0.1/", ["10.0.0.1"]), ("http://172.16.0.1/", ["172.16.0.1"]), ("http://192.168.1.1/", ["192.168.1.1"]), ("http://169.254.169.254/", ["169.254.169.254"]), ("http://localhost/", ["127.0.0.1", "::1"]), ("http://internal.example/", ["192.168.1.10"]), ("https://example.com/", ["93.184.216.34", "192.168.1.10"]), ], ) def test_is_public_url_returns_false_for_non_public_addresses( resolve_name: Mock, url: str, resolved_addresses: list[str] ) -> None: resolve_name.return_value.addresses.return_value = resolved_addresses assert not is_public_url(url) @patch("webmentions_ssg.url_security.dns.resolver.resolve_name") @pytest.mark.parametrize( ("url", "resolved_addresses"), [ ("https://example.com/", ["93.184.216.34"]), ( "https://example.com/", ["93.184.216.34", "2606:2800:220:1:248:1893:25c8:1946"], ), ], ) def test_is_public_url_returns_true_for_public_addresses( resolve_name: Mock, url: str, resolved_addresses: list[str] ) -> None: resolve_name.return_value.addresses.return_value = resolved_addresses assert is_public_url(url) @patch( "webmentions_ssg.url_security.dns.resolver.resolve_name", side_effect=dns.resolver.NXDOMAIN(), ) def test_is_public_url_raises_for_resolution_failure(resolve_name: Mock) -> None: with pytest.raises(AddressResolutionError, match="Could not resolve hostname"): is_public_url("https://nonexistent.example/") resolve_name.assert_called_once_with("nonexistent.example") @patch("webmentions_ssg.url_security.dns.resolver.resolve_name") def test_is_public_url_raises_when_url_has_no_hostname(resolve_name: Mock) -> None: with pytest.raises(ValueError, match="No hostname was specified"): is_public_url("/relative/url") resolve_name.assert_not_called()