kanban-app/backend/tests/routes/test_host_validation.py
2026-08-09 22:02:18 +03:00

97 lines
No EOL
3.7 KiB
Python

"""Tests for the ALLOWED_HOSTS host validation."""
import pytest
def _run_before_request(app, path: str, host: str):
"""Invoke the registered before_request hooks for the given path/host.
Returns the Flask response tuple (response, status) if a hook blocked the
request, or None if the request passed through.
We test the hook directly (instead of via the HTTP test client) because
the test app config sets SERVER_NAME="localhost.localdomain", which makes
Flask's routing layer reject foreign Host headers with 404 before any
before_request hook runs.
"""
with app.test_request_context(path, headers={"Host": host}):
for func in app.before_request_funcs.get(None, []):
result = func()
if result is not None:
return result
return None
@pytest.fixture(autouse=True)
def _restore_allowed_hosts(app):
"""Restore the original ALLOWED_HOSTS after each test.
The app fixture is session-scoped, so mutations to app.config persist
across tests/files. This ensures host validation doesn't leak into
other test files.
"""
original = app.config.get("ALLOWED_HOSTS")
yield
app.config["ALLOWED_HOSTS"] = original
@pytest.mark.integration
class TestHostValidation:
"""Test the before_request ALLOWED_HOSTS enforcement."""
def test_allowed_host_accepted(self, app):
"""Requests with an allowed Host header pass through."""
app.config["ALLOWED_HOSTS"] = ["example.com", "www.example.com"]
result = _run_before_request(app, "/api/boards", "example.com")
assert result is None
def test_allowed_host_with_port_accepted(self, app):
"""Host header with a port is stripped and validated."""
app.config["ALLOWED_HOSTS"] = ["example.com"]
result = _run_before_request(app, "/api/boards", "example.com:8000")
assert result is None
def test_disallowed_host_rejected(self, app):
"""Requests with a disallowed Host header are rejected with 404."""
app.config["ALLOWED_HOSTS"] = ["example.com"]
result = _run_before_request(app, "/api/boards", "192.168.1.135:8000")
assert result is not None
assert result[1] == 404
assert result[0].get_json()["error"] == "Not found"
def test_unknown_domain_rejected(self, app):
"""Unknown domains are rejected, not just IPs."""
app.config["ALLOWED_HOSTS"] = ["example.com"]
result = _run_before_request(app, "/api/boards", "evil-site.com")
assert result is not None
assert result[1] == 404
def test_no_allowed_hosts_permissive(self, app):
"""Empty ALLOWED_HOSTS disables the check (backwards compatible)."""
app.config["ALLOWED_HOSTS"] = []
result = _run_before_request(app, "/api/boards", "192.168.1.135:8000")
assert result is None
def test_case_insensitive_host_match(self, app):
"""Host matching is case-insensitive."""
app.config["ALLOWED_HOSTS"] = ["example.com"]
result = _run_before_request(app, "/api/boards", "EXAMPLE.COM")
assert result is None
def test_health_endpoint_exempt(self, app):
"""Health checks are exempt from host validation."""
app.config["ALLOWED_HOSTS"] = ["example.com"]
result = _run_before_request(app, "/health/", "localhost:8000")
assert result is None
def test_parse_list_helper(self):
"""_parse_list splits, trims, lowercases, and drops empties."""
from app.config import _parse_list
assert _parse_list("") == []
assert _parse_list(None) == []
assert _parse_list("Example.com, WWW.Example.com , ,other.org") == [
"example.com",
"www.example.com",
"other.org",
]