Files
DECNET/tests/geoip/test_ptr.py
anti f2b3393669 chore: relicense to AGPL-3.0-or-later and add SPDX headers
Replaces LICENSE (GPLv3 -> AGPLv3) and prepends
`SPDX-License-Identifier: AGPL-3.0-or-later` to every source file
across decnet/, decnet_web/, tests/, scripts/, and tools/.

Rationale: closes the GPLv3 ASP loophole so any party operating a
modified DECNET as a network service must offer their modified
source. Personal copyright (Samuel Paschuan) + inbound=outbound
contributions make a future unilateral relicense infeasible.

- LICENSE: full AGPL-3.0 text (gnu.org/licenses/agpl-3.0.txt)
- COPYRIGHT: project copyright notice
- tools/add_spdx_headers.py: idempotent header injector
  (shebang- and PEP 263-aware)

Touches 1565 source files (.py, .ts, .tsx, .js, .jsx, .css, .sh).
No behavior change; comments only.
2026-05-22 21:04:16 -04:00

121 lines
3.7 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-or-later
"""Unit tests for decnet.geoip.ptr — reverse-DNS resolver."""
from __future__ import annotations
import asyncio
import socket
from unittest.mock import patch
import pytest
from decnet.geoip.ptr import _is_resolvable, resolve_ptr_record
@pytest.fixture(autouse=True)
def _enable_ptr(monkeypatch):
"""This module covers the resolver directly — re-enable the env
switch that tests/conftest.py disables globally."""
monkeypatch.setenv("DECNET_PTR_ENABLED", "true")
# ─── pure predicate ─────────────────────────────────────────────────────────
@pytest.mark.parametrize("ip", [
"127.0.0.1",
"10.0.0.1",
"192.168.1.5",
"172.16.0.1",
"169.254.1.1", # link-local
"224.0.0.1", # multicast
"::1",
"fe80::1", # IPv6 link-local
"not-an-ip",
"",
])
def test_not_resolvable(ip: str):
assert _is_resolvable(ip) is False
@pytest.mark.parametrize("ip", [
"8.8.8.8",
"1.1.1.1",
"2606:4700:4700::1111",
])
def test_resolvable_public(ip: str):
assert _is_resolvable(ip) is True
# ─── resolver ───────────────────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_resolves_public_ip():
with patch(
"decnet.geoip.ptr.socket.gethostbyaddr",
return_value=("dns.google", [], ["8.8.8.8"]),
):
name = await resolve_ptr_record("8.8.8.8")
assert name == "dns.google"
@pytest.mark.asyncio
async def test_private_ip_short_circuits():
"""Private IPs never touch the resolver."""
with patch("decnet.geoip.ptr.socket.gethostbyaddr") as mock_lookup:
assert await resolve_ptr_record("127.0.0.1") is None
assert await resolve_ptr_record("10.0.0.1") is None
assert await resolve_ptr_record("::1") is None
assert mock_lookup.call_count == 0
@pytest.mark.asyncio
async def test_gethostbyaddr_herror_returns_none():
with patch(
"decnet.geoip.ptr.socket.gethostbyaddr",
side_effect=socket.herror("no rDNS"),
):
assert await resolve_ptr_record("8.8.8.8") is None
@pytest.mark.asyncio
async def test_gethostbyaddr_gaierror_returns_none():
with patch(
"decnet.geoip.ptr.socket.gethostbyaddr",
side_effect=socket.gaierror("dns broken"),
):
assert await resolve_ptr_record("8.8.8.8") is None
@pytest.mark.asyncio
async def test_timeout_returns_none():
"""A slow resolver should not block the caller past timeout."""
def slow(ip: str): # noqa: ARG001
import time
time.sleep(3.0)
return ("slow.example", [], [])
with patch("decnet.geoip.ptr.socket.gethostbyaddr", side_effect=slow):
# Tight timeout — must return quickly.
result = await asyncio.wait_for(
resolve_ptr_record("8.8.8.8", timeout=0.1),
timeout=1.0,
)
assert result is None
@pytest.mark.asyncio
async def test_env_disabled(monkeypatch):
monkeypatch.setenv("DECNET_PTR_ENABLED", "false")
with patch("decnet.geoip.ptr.socket.gethostbyaddr") as mock_lookup:
assert await resolve_ptr_record("8.8.8.8") is None
assert mock_lookup.call_count == 0
@pytest.mark.asyncio
async def test_empty_hostname_returned_as_none():
"""gethostbyaddr can return '' on some platforms; normalize to None."""
with patch(
"decnet.geoip.ptr.socket.gethostbyaddr",
return_value=("", [], ["8.8.8.8"]),
):
assert await resolve_ptr_record("8.8.8.8") is None