Critical: the MCP SDK auto-enables DNS-rebinding protection when its host parameter is left at the 127.0.0.1 default, hard-coding a loopback-only Host allow-list. Every remote client (the only deployment this feature targets) was refused with HTTP 421 before auth or the tool ran. Now driven by a new MCP_ALLOWED_HOSTS setting, with '*' as an explicit, logged opt-out. Also bounds query/top_k to match AskRequest (top_k is amplified 4x downstream, so an unbounded value was a resource-exhaustion vector), decodes the Authorization header as latin-1 per the ASGI spec instead of raising a 500 on malformed bytes, and returns WWW-Authenticate on 401 per RFC 7235. Moves the psycopg2 import guard into backend/tests/conftest.py: duplicated across four test modules, it only worked because of alphabetical collection order, and any earlier-sorting package would have reintroduced a live connection attempt against the production database. Registers the mcp module in the authoritative backend architecture doc. 84 backend tests pass. Verified against a live server: allowed remote Host returns a valid initialize result, unknown Host returns 421, missing token returns 401 with WWW-Authenticate. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
93 lines
2.9 KiB
Python
93 lines
2.9 KiB
Python
"""Unit tests for PostgresEventStore using a mocked psycopg2 pool."""
|
|
from __future__ import annotations
|
|
import json
|
|
from unittest.mock import MagicMock, patch
|
|
import pytest
|
|
|
|
# psycopg2 is mocked centrally in backend/tests/conftest.py, so importing the
|
|
# module under test here never binds the real driver.
|
|
from app.infrastructure.perception.base_event_store import BaseEventStore
|
|
|
|
|
|
SAMPLE_ROW = {
|
|
"id": "pg-001",
|
|
"source": "国标委",
|
|
"source_label": "国家标准化管理委员会",
|
|
"standard_code": "GB 18384-2025",
|
|
"title": "电动汽车安全要求",
|
|
"summary": "新增要求",
|
|
"full_text_url": "https://openstd.samr.gov.cn",
|
|
"status": "enacted",
|
|
"impact_level": "high",
|
|
"published_at": "2025-11-15",
|
|
"effective_at": "2026-07-01",
|
|
"category": "电动汽车安全",
|
|
"tags": ["电池安全"],
|
|
"obligations": None,
|
|
"deadlines": None,
|
|
"scope": None,
|
|
"penalties": None,
|
|
"content_hash": "abc123",
|
|
"previous_hash": None,
|
|
"change_summary": None,
|
|
"changed_sections": None,
|
|
"affected_docs": None,
|
|
"crawled_at": "2026-06-05T10:00:00+00:00",
|
|
"processed_at": None,
|
|
"raw_storage_key": None,
|
|
}
|
|
|
|
|
|
def _make_store_with_pool(mock_pool):
|
|
with patch("psycopg2.pool.ThreadedConnectionPool", return_value=mock_pool):
|
|
with patch(
|
|
"app.infrastructure.perception.postgres_event_store.PostgresEventStore._ensure_schema"
|
|
):
|
|
from app.infrastructure.perception.postgres_event_store import PostgresEventStore
|
|
return PostgresEventStore()
|
|
|
|
|
|
def _cursor_returning(rows):
|
|
cursor = MagicMock()
|
|
cursor.__enter__ = lambda s: s
|
|
cursor.__exit__ = MagicMock(return_value=False)
|
|
cursor.fetchall.return_value = rows
|
|
cursor.fetchone.return_value = rows[0] if rows else None
|
|
return cursor
|
|
|
|
|
|
def test_is_base_event_store():
|
|
mock_pool = MagicMock()
|
|
store = _make_store_with_pool(mock_pool)
|
|
assert isinstance(store, BaseEventStore)
|
|
|
|
|
|
def test_filter_returns_list():
|
|
mock_pool = MagicMock()
|
|
conn = MagicMock()
|
|
conn.__enter__ = lambda s: s
|
|
conn.__exit__ = MagicMock(return_value=False)
|
|
cursor = _cursor_returning([SAMPLE_ROW])
|
|
conn.cursor.return_value = cursor
|
|
mock_pool.getconn.return_value = conn
|
|
store = _make_store_with_pool(mock_pool)
|
|
result = store.filter(limit=10)
|
|
assert isinstance(result, list)
|
|
|
|
|
|
def test_stats_returns_correct_keys():
|
|
mock_pool = MagicMock()
|
|
conn = MagicMock()
|
|
conn.__enter__ = lambda s: s
|
|
conn.__exit__ = MagicMock(return_value=False)
|
|
cursor = MagicMock()
|
|
cursor.__enter__ = lambda s: s
|
|
cursor.__exit__ = MagicMock(return_value=False)
|
|
cursor.fetchone.return_value = {"count": 5}
|
|
conn.cursor.return_value = cursor
|
|
mock_pool.getconn.return_value = conn
|
|
store = _make_store_with_pool(mock_pool)
|
|
stats = store.stats()
|
|
for key in ("total", "high_impact", "medium_impact", "recent_90d"):
|
|
assert key in stats
|