Spaces:
Paused
Paused
| import ipaddress | |
| import importlib.util | |
| import sys | |
| import types | |
| from pathlib import Path | |
| import pytest | |
| def test_public_url_validator_blocks_internal_targets(url): | |
| from src.url_security import is_public_http_url | |
| assert is_public_http_url(url) is False | |
| def test_public_url_validator_allows_public_endpoint(monkeypatch): | |
| from src import url_security | |
| monkeypatch.setattr( | |
| url_security, | |
| "_resolve_hostname_ips", | |
| lambda host: [ipaddress.ip_address("93.184.216.34")], | |
| ) | |
| assert url_security.validate_public_http_url("https://api.example.com/v1") == "https://api.example.com/v1" | |
| def test_public_url_validator_blocks_dns_to_private(monkeypatch): | |
| from src import url_security | |
| monkeypatch.setattr( | |
| url_security, | |
| "_resolve_hostname_ips", | |
| lambda host: [ipaddress.ip_address("10.0.0.5")], | |
| ) | |
| with pytest.raises(ValueError): | |
| url_security.validate_public_http_url("https://api.example.com/v1") | |
| def _load_webhook_routes_for_test(monkeypatch): | |
| # Load under a unique module name so each test gets a fresh module object | |
| # rather than a cached one from a previous monkeypatch run. | |
| core_pkg = types.ModuleType("core") | |
| core_pkg.__path__ = [] | |
| core_db = types.ModuleType("core.database") | |
| core_db.SessionLocal = object | |
| core_db.Webhook = object | |
| core_db.ModelEndpoint = object | |
| core_middleware = types.ModuleType("core.middleware") | |
| core_middleware.require_admin = lambda request: None | |
| webhook_manager = types.ModuleType("src.webhook_manager") | |
| webhook_manager.WebhookManager = object | |
| webhook_manager.validate_webhook_url = lambda url: url | |
| webhook_manager.validate_events = lambda events: events | |
| monkeypatch.setitem(sys.modules, "core", core_pkg) | |
| monkeypatch.setitem(sys.modules, "core.database", core_db) | |
| monkeypatch.setitem(sys.modules, "core.middleware", core_middleware) | |
| monkeypatch.setitem(sys.modules, "src.webhook_manager", webhook_manager) | |
| module_name = "routes.webhook_routes_under_test" | |
| spec = importlib.util.spec_from_file_location( | |
| module_name, | |
| Path(__file__).resolve().parent.parent / "routes" / "webhook_routes.py", | |
| ) | |
| module = importlib.util.module_from_spec(spec) | |
| spec.loader.exec_module(module) | |
| return module | |
| class _Expr: | |
| def __init__(self, fn): | |
| self.fn = fn | |
| def __call__(self, row): | |
| return self.fn(row) | |
| def __or__(self, other): | |
| return _Expr(lambda row: self(row) or other(row)) | |
| class _Column: | |
| def __init__(self, name): | |
| self.name = name | |
| def __eq__(self, other): | |
| return _Expr(lambda row: getattr(row, self.name) == other) | |
| def desc(self): | |
| return ("desc", self.name) | |
| class _ModelEndpoint: | |
| is_enabled = _Column("is_enabled") | |
| owner = _Column("owner") | |
| created_at = _Column("created_at") | |
| class _Endpoint: | |
| def __init__( | |
| self, | |
| *, | |
| owner, | |
| is_enabled=True, | |
| created_at=1, | |
| base_url="https://api.example.com/v1", | |
| api_key=None, | |
| ): | |
| self.owner = owner | |
| self.is_enabled = is_enabled | |
| self.created_at = created_at | |
| self.base_url = base_url | |
| self.api_key = api_key | |
| class _EndpointQuery: | |
| def __init__(self, rows): | |
| self.rows = rows | |
| self.filters = [] | |
| self.orders = [] | |
| def filter(self, *exprs): | |
| self.filters.extend(exprs) | |
| return self | |
| def order_by(self, *exprs): | |
| self.orders.extend(exprs) | |
| return self | |
| def first(self): | |
| rows = self.rows | |
| for expr in self.filters: | |
| rows = [row for row in rows if expr(row)] | |
| # Apply sort keys right-to-left so the leftmost key ends up as the | |
| # primary sort (stable-sort reversal idiom mirrors SQLAlchemy's | |
| # multi-column ORDER BY behaviour). | |
| for order in reversed(self.orders): | |
| reverse = False | |
| name = getattr(order, "name", None) | |
| if isinstance(order, tuple) and order[0] == "desc": | |
| reverse = True | |
| name = order[1] | |
| rows = sorted(rows, key=lambda row: getattr(row, name) is not None, reverse=reverse) | |
| if name != "owner": | |
| rows = sorted(rows, key=lambda row: getattr(row, name), reverse=reverse) | |
| return rows[0] if rows else None | |
| class _DB: | |
| def __init__(self, rows): | |
| self.query_obj = _EndpointQuery(rows) | |
| self.closed = False | |
| def query(self, model): | |
| assert model is _ModelEndpoint | |
| return self.query_obj | |
| def close(self): | |
| self.closed = True | |
| class _ChatSession: | |
| def __init__(self, endpoint_url, model): | |
| self.endpoint_url = endpoint_url | |
| self.model = model | |
| self.headers = {} | |
| self.history = [] | |
| def add_message(self, message): | |
| self.history.append(message) | |
| class _SessionManager: | |
| def __init__(self): | |
| self.created = [] | |
| self.save_calls = 0 | |
| def create_session(self, *, session_id, name, endpoint_url, model, owner): | |
| session = _ChatSession(endpoint_url, model) | |
| self.created.append({ | |
| "session_id": session_id, | |
| "name": name, | |
| "endpoint_url": endpoint_url, | |
| "model": model, | |
| "owner": owner, | |
| "session": session, | |
| }) | |
| return session | |
| def save_sessions(self): | |
| self.save_calls += 1 | |
| class _Request: | |
| def __init__(self, *, owner="alice"): | |
| self.state = types.SimpleNamespace( | |
| api_token=True, | |
| api_token_scopes=["chat"], | |
| api_token_owner=owner, | |
| ) | |
| class _WebhookManager: | |
| async def fire(self, event, payload): | |
| return None | |
| def _install_sync_chat_stubs(monkeypatch): | |
| # FastAPI checks for python_multipart at import time when Form is used; | |
| # stub it so the optional dependency is not required in the test environment. | |
| python_multipart = types.ModuleType("python_multipart") | |
| python_multipart.__version__ = "0.0.13" | |
| core_models = types.ModuleType("core.models") | |
| class _ChatMessage: | |
| def __init__(self, role, content): | |
| self.role = role | |
| self.content = content | |
| async def _llm_call_async(endpoint_url, model, messages, headers=None, timeout=None): | |
| return "mocked response" | |
| endpoint_resolver = types.ModuleType("src.endpoint_resolver") | |
| endpoint_resolver.normalize_base = lambda url: (url or "").strip().rstrip("/") | |
| endpoint_resolver.build_chat_url = lambda base_url: f"{base_url}/chat/completions" | |
| endpoint_resolver.build_models_url = lambda base_url: f"{base_url}/models" | |
| endpoint_resolver.build_headers = lambda api_key, base_url: {"Authorization": f"Bearer {api_key}"} | |
| llm_core = types.ModuleType("src.llm_core") | |
| llm_core.llm_call_async = _llm_call_async | |
| core_models.ChatMessage = _ChatMessage | |
| monkeypatch.setitem(sys.modules, "python_multipart", python_multipart) | |
| monkeypatch.setitem(sys.modules, "core.models", core_models) | |
| monkeypatch.setitem(sys.modules, "src.llm_core", llm_core) | |
| monkeypatch.setitem(sys.modules, "src.endpoint_resolver", endpoint_resolver) | |
| def _sync_chat_endpoint(webhook_routes, session_manager): | |
| router = webhook_routes.setup_webhook_routes( | |
| _WebhookManager(), | |
| auth_manager=None, | |
| session_manager=session_manager, | |
| ) | |
| for route in router.routes: | |
| if route.path == "/api/v1/chat": | |
| return route.endpoint | |
| raise AssertionError("sync chat route not found") | |
| async def test_api_chat_direct_base_url_rejects_local_private_targets(monkeypatch, base_url): | |
| webhook_routes = _load_webhook_routes_for_test(monkeypatch) | |
| _install_sync_chat_stubs(monkeypatch) | |
| session_manager = _SessionManager() | |
| sync_chat = _sync_chat_endpoint(webhook_routes, session_manager) | |
| body = types.SimpleNamespace( | |
| message="hello", | |
| api_key="test-key", | |
| base_url=base_url, | |
| model="test-model", | |
| provider=None, | |
| session=None, | |
| ) | |
| with pytest.raises(webhook_routes.HTTPException) as exc: | |
| await sync_chat(_Request(), body) | |
| assert exc.value.status_code == 400 | |
| assert exc.value.detail == "base_url must point to a public HTTP(S) endpoint" | |
| assert session_manager.created == [] | |
| async def test_api_chat_direct_base_url_allows_mocked_public_endpoint(monkeypatch): | |
| webhook_routes = _load_webhook_routes_for_test(monkeypatch) | |
| _install_sync_chat_stubs(monkeypatch) | |
| from src import url_security | |
| monkeypatch.setattr( | |
| url_security, | |
| "_resolve_hostname_ips", | |
| lambda host: [ipaddress.ip_address("93.184.216.34")], | |
| ) | |
| session_manager = _SessionManager() | |
| sync_chat = _sync_chat_endpoint(webhook_routes, session_manager) | |
| body = types.SimpleNamespace( | |
| message="hello", | |
| api_key="test-key", | |
| base_url="https://api.example.com/v1", | |
| model="test-model", | |
| provider=None, | |
| session=None, | |
| ) | |
| response = await sync_chat(_Request(), body) | |
| assert response["response"] == "mocked response" | |
| assert response["model"] == "test-model" | |
| assert session_manager.created[0]["endpoint_url"] == "https://api.example.com/v1/chat/completions" | |
| def test_api_chat_fallback_endpoint_selection_for_owned_token(monkeypatch): | |
| webhook_routes = _load_webhook_routes_for_test(monkeypatch) | |
| rows = [ | |
| _Endpoint(owner="alice", is_enabled=False, created_at=0), | |
| _Endpoint(owner="bob", created_at=0), | |
| _Endpoint(owner=None, created_at=1), | |
| _Endpoint(owner="alice", created_at=2), | |
| ] | |
| monkeypatch.setattr(webhook_routes, "ModelEndpoint", _ModelEndpoint) | |
| selected = webhook_routes._select_api_chat_fallback_endpoint(_DB(rows), "alice") | |
| assert selected.owner == "alice" | |
| assert selected.is_enabled is True | |
| assert selected.created_at == 2 | |
| def test_api_chat_fallback_without_owner_uses_shared_only(monkeypatch): | |
| webhook_routes = _load_webhook_routes_for_test(monkeypatch) | |
| rows = [ | |
| _Endpoint(owner="alice", created_at=0), | |
| _Endpoint(owner=None, is_enabled=False, created_at=1), | |
| _Endpoint(owner=None, created_at=2), | |
| ] | |
| monkeypatch.setattr(webhook_routes, "ModelEndpoint", _ModelEndpoint) | |
| selected = webhook_routes._select_api_chat_fallback_endpoint(_DB(rows), None) | |
| assert selected.owner is None | |
| assert selected.is_enabled is True | |
| assert selected.created_at == 2 | |
| async def test_api_chat_fallback_trusts_configured_local_endpoint(monkeypatch): | |
| webhook_routes = _load_webhook_routes_for_test(monkeypatch) | |
| _install_sync_chat_stubs(monkeypatch) | |
| local_endpoint = _Endpoint( | |
| owner=None, | |
| base_url="http://localhost:11434/v1", | |
| api_key="configured-key", | |
| ) | |
| db = _DB([local_endpoint]) | |
| calls = [] | |
| def _session_local(): | |
| return db | |
| def _validate_public_http_url(url, *, max_length=2048): | |
| calls.append(url) | |
| raise AssertionError("configured fallback endpoint should not be publicly validated") | |
| monkeypatch.setattr(webhook_routes, "ModelEndpoint", _ModelEndpoint) | |
| monkeypatch.setattr(webhook_routes, "SessionLocal", _session_local) | |
| monkeypatch.setattr(webhook_routes, "validate_public_http_url", _validate_public_http_url) | |
| session_manager = _SessionManager() | |
| sync_chat = _sync_chat_endpoint(webhook_routes, session_manager) | |
| body = types.SimpleNamespace( | |
| message="hello", | |
| model="local-model", | |
| api_key=None, | |
| base_url=None, | |
| provider=None, | |
| session=None, | |
| ) | |
| response = await sync_chat(_Request(owner=None), body) | |
| assert response["response"] == "mocked response" | |
| assert response["model"] == "local-model" | |
| assert session_manager.created[0]["endpoint_url"] == "http://localhost:11434/v1/chat/completions" | |
| assert calls == [] | |