Files
wehub-resource-sync 09e9f3545f
Test / Code Quality (push) Has been cancelled
Test / Test (macos-latest, Python 3.10) (push) Has been cancelled
Test / Test (macos-latest, Python 3.11) (push) Has been cancelled
Test / Test (macos-latest, Python 3.12) (push) Has been cancelled
Test / Test (macos-latest, Python 3.13) (push) Has been cancelled
Test / Test (macos-latest, Python 3.14) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.10) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.11) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.12) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.13) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.14) (push) Has been cancelled
Test / Test (windows-latest, Python 3.10) (push) Has been cancelled
Test / Test (windows-latest, Python 3.11) (push) Has been cancelled
Test / Test (windows-latest, Python 3.12) (push) Has been cancelled
Test / Test (windows-latest, Python 3.13) (push) Has been cancelled
Test / Test (windows-latest, Python 3.14) (push) Has been cancelled
CodeQL / Analyze (push) Has been cancelled
dependency-audit / pip-audit (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:30:13 +08:00

114 lines
3.9 KiB
Python

"""Tests for the shared server-bootstrap helpers (``notebooklm._serving``).
This is the single source for loopback classification + the non-loopback bind
guard used by BOTH the ``notebooklm-mcp`` and ``notebooklm-server`` entry points
(they drifted before #1769). The `::ffff:127.0.0.1` cases pin the IPv4-mapped-IPv6
fix that the two naive per-server copies previously lacked.
"""
from __future__ import annotations
import ast
from pathlib import Path
import pytest
from notebooklm import _serving
@pytest.mark.parametrize(
("host", "expected"),
[
("127.0.0.1", True),
("::1", True),
("localhost", True),
("LOCALHOST", True), # case-insensitive
(" localhost ", True), # surrounding whitespace tolerated
("::ffff:127.0.0.1", True), # IPv4-mapped IPv6 loopback (the #1769 fix)
("::ffff:7f00:1", True), # same, hex form
("0.0.0.0", False),
("::", False),
("203.0.113.5", False),
("example.com", False), # non-IP hostname is never loopback
("", False),
],
)
def test_is_loopback(host: str, expected: bool) -> None:
assert _serving.is_loopback(host) is expected
@pytest.mark.parametrize(
"host_header, expected",
[
("127.0.0.1", True),
("127.0.0.1:9420", True),
("localhost", True),
("LocalHost:8080", True),
("[::1]", True),
("[::1]:9420", True),
("evil.example", False),
("evil.example:9420", False),
("127.0.0.1.evil.com", False),
("[::1]evil.com", False),
("0.0.0.0", False),
("", False),
(" ", False),
],
)
def test_host_header_is_loopback(host_header: str, expected: bool) -> None:
assert _serving.host_header_is_loopback(host_header) is expected
def test_addr_is_loopback_ipv4_mapped() -> None:
# The primitive server/_auth._addr_is_loopback delegates to — the request-path
# loopback check must agree with the bind-path one on the mapped form.
assert _serving.addr_is_loopback("::ffff:127.0.0.1") is True
assert _serving.addr_is_loopback("not-an-ip") is False
def test_check_bind_allowed_empty_host_hard_refuses_even_with_override() -> None:
with pytest.raises(SystemExit, match="empty host"):
_serving.check_bind_allowed(
" ", allow_external=True, what="the X", allow_env="X_ALLOW_EXTERNAL_BIND"
)
def test_check_bind_allowed_loopback_ok() -> None:
_serving.check_bind_allowed(
"127.0.0.1", allow_external=False, what="the X", allow_env="X_ALLOW"
)
_serving.check_bind_allowed(
"::ffff:127.0.0.1", allow_external=False, what="the X", allow_env="X_ALLOW"
)
def test_check_bind_allowed_non_loopback_refused_names_the_allow_env() -> None:
with pytest.raises(SystemExit, match="MY_OVERRIDE_ENV"):
_serving.check_bind_allowed(
"0.0.0.0", allow_external=False, what="the X", allow_env="MY_OVERRIDE_ENV"
)
def test_check_bind_allowed_non_loopback_allowed_with_override() -> None:
_serving.check_bind_allowed(
"203.0.113.5", allow_external=True, what="the X", allow_env="X_ALLOW"
)
def test_serving_imports_no_cli_click_rich() -> None:
"""The MCP stdio entry point imports this module; it must stay free of
``click``/``rich``/``cli`` (import surface + pristine stdout). The dir-scoped
mcp/server boundary guards don't cover this top-level module, so assert directly."""
src = Path(_serving.__file__).read_text(encoding="utf-8")
tree = ast.parse(src)
imported: list[str] = []
for node in ast.walk(tree):
if isinstance(node, ast.Import):
imported += [a.name for a in node.names]
elif isinstance(node, ast.ImportFrom):
imported.append(node.module or "")
forbidden = [
m for m in imported if m.split(".")[0] in {"click", "rich"} or "cli" in m.split(".")
]
assert not forbidden, f"_serving.py must not import click/rich/cli, found: {forbidden}"