7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
649 lines
23 KiB
Python
649 lines
23 KiB
Python
"""
|
|
Coverage tests for api_routes.py targeting uncovered statements and branches.
|
|
|
|
Covers:
|
|
- get_current_config: exception path (lines 64-66)
|
|
- api_research_status: exception path (lines 117-119)
|
|
- api_terminate_research: exception path (lines 151-153)
|
|
- api_get_resources: exception path (lines 168-170)
|
|
- api_add_resource: SSRF rejection (lines 207-208)
|
|
- api_add_resource: success path with add_resource call (lines 227-245)
|
|
- api_add_resource: exception path (lines 243-247)
|
|
- api_delete_resource: exception path (lines 274-276)
|
|
- check_ollama_status: old API format (lines 342-343)
|
|
- check_ollama_status: invalid JSON response (lines 354-358)
|
|
- check_ollama_status: non-200 status code (lines 364-368)
|
|
- check_ollama_status: timeout error (lines 386-395)
|
|
- check_ollama_status: general exception (lines 397-406)
|
|
- check_ollama_model: non-200 status (lines 460-463)
|
|
- check_ollama_model: old API format (lines 492-493)
|
|
- check_ollama_model: no models found (lines 525-526)
|
|
- check_ollama_model: JSON parse error (lines 542-554)
|
|
- check_ollama_model: connection error (lines 556-567)
|
|
- check_ollama_model: timeout error (lines 568-578)
|
|
- check_ollama_model: general exception (lines 580-596)
|
|
"""
|
|
|
|
import requests
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
from flask import Flask
|
|
|
|
from local_deep_research.web.auth import auth_bp
|
|
from local_deep_research.web.routes.api_routes import api_bp
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Constants
|
|
# ---------------------------------------------------------------------------
|
|
|
|
MODULE = "local_deep_research.web.routes.api_routes"
|
|
AUTH_DB_MANAGER = "local_deep_research.web.auth.decorators.db_manager"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _mock_auth():
|
|
"""Return a MagicMock that satisfies login_required db_manager check."""
|
|
return MagicMock(is_user_connected=MagicMock(return_value=True))
|
|
|
|
|
|
def _make_db_ctx(mock_session):
|
|
"""Build a mock context-manager for get_user_db_session."""
|
|
ctx = MagicMock()
|
|
ctx.__enter__ = MagicMock(return_value=mock_session)
|
|
ctx.__exit__ = MagicMock(return_value=None)
|
|
return ctx
|
|
|
|
|
|
def _make_db_ctx_raising(exc):
|
|
"""Build a context-manager that raises on __enter__."""
|
|
ctx = MagicMock()
|
|
ctx.__enter__ = MagicMock(side_effect=exc)
|
|
ctx.__exit__ = MagicMock(return_value=None)
|
|
return ctx
|
|
|
|
|
|
def _build_filter_chain(result):
|
|
"""Build chained SQLAlchemy mock query for filter_by().first()."""
|
|
q = MagicMock()
|
|
q.filter_by.return_value.first.return_value = result
|
|
return q
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Fixtures
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.fixture()
|
|
def app():
|
|
"""Minimal Flask app with auth and api blueprints."""
|
|
application = Flask(__name__)
|
|
application.config["SECRET_KEY"] = "test-secret"
|
|
application.config["TESTING"] = True
|
|
application.register_blueprint(auth_bp)
|
|
application.register_blueprint(api_bp)
|
|
return application
|
|
|
|
|
|
def _authed_get(app, path, **kwargs):
|
|
"""Issue an authenticated GET request and return the response."""
|
|
with app.test_client() as c:
|
|
with c.session_transaction() as sess:
|
|
sess["username"] = "testuser"
|
|
return c.get(path, **kwargs)
|
|
|
|
|
|
def _authed_post(app, path, **kwargs):
|
|
"""Issue an authenticated POST request and return the response."""
|
|
with app.test_client() as c:
|
|
with c.session_transaction() as sess:
|
|
sess["username"] = "testuser"
|
|
return c.post(path, **kwargs)
|
|
|
|
|
|
def _authed_delete(app, path, **kwargs):
|
|
"""Issue an authenticated DELETE request and return the response."""
|
|
with app.test_client() as c:
|
|
with c.session_transaction() as sess:
|
|
sess["username"] = "testuser"
|
|
return c.delete(path, **kwargs)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# get_current_config: exception path (lines 64-66)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestGetCurrentConfigException:
|
|
"""Exception path in get_current_config."""
|
|
|
|
def test_get_current_config_exception_returns_500(self, app):
|
|
"""When get_user_db_session raises, return 500 with error."""
|
|
with patch(AUTH_DB_MANAGER, _mock_auth()):
|
|
with patch(
|
|
f"{MODULE}.get_user_db_session",
|
|
return_value=_make_db_ctx_raising(RuntimeError("db down")),
|
|
):
|
|
resp = _authed_get(app, "/settings/current-config")
|
|
assert resp.status_code == 500
|
|
data = resp.get_json()
|
|
assert data["success"] is False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# api_research_status: exception path (lines 117-119)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestApiResearchStatusException:
|
|
"""Exception path in api_research_status."""
|
|
|
|
def test_research_status_exception_returns_500(self, app):
|
|
"""When db session raises, return 500 error."""
|
|
with patch(AUTH_DB_MANAGER, _mock_auth()):
|
|
with patch(
|
|
f"{MODULE}.get_user_db_session",
|
|
return_value=_make_db_ctx_raising(RuntimeError("db error")),
|
|
):
|
|
resp = _authed_get(app, "/status/some-id")
|
|
assert resp.status_code == 500
|
|
data = resp.get_json()
|
|
assert data["status"] == "error"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# api_terminate_research: exception path (lines 151-153)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestApiTerminateResearchException:
|
|
"""Exception path in api_terminate_research."""
|
|
|
|
def test_terminate_exception_returns_500(self, app):
|
|
"""When cancel_research raises, return 500."""
|
|
with patch(AUTH_DB_MANAGER, _mock_auth()):
|
|
with patch(
|
|
f"{MODULE}.cancel_research",
|
|
side_effect=RuntimeError("cancel boom"),
|
|
):
|
|
resp = _authed_post(app, "/terminate/some-id")
|
|
assert resp.status_code == 500
|
|
data = resp.get_json()
|
|
assert data["status"] == "error"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# api_get_resources: exception path (lines 168-170)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestApiGetResourcesException:
|
|
"""Exception path in api_get_resources."""
|
|
|
|
def test_get_resources_exception_returns_500(self, app):
|
|
"""When get_resources_for_research raises, return 500."""
|
|
with patch(AUTH_DB_MANAGER, _mock_auth()):
|
|
with patch(
|
|
f"{MODULE}.get_resources_for_research",
|
|
side_effect=RuntimeError("res err"),
|
|
):
|
|
resp = _authed_get(app, "/resources/some-id")
|
|
assert resp.status_code == 500
|
|
data = resp.get_json()
|
|
assert data["status"] == "error"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# api_add_resource: SSRF rejection (lines 207-208)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestApiAddResourceSsrf:
|
|
"""SSRF URL rejection in api_add_resource."""
|
|
|
|
def test_ssrf_invalid_url_rejected(self, app):
|
|
"""When validate_url returns False, return 400."""
|
|
with patch(AUTH_DB_MANAGER, _mock_auth()):
|
|
with patch(
|
|
"local_deep_research.security.ssrf_validator.validate_url",
|
|
return_value=False,
|
|
):
|
|
resp = _authed_post(
|
|
app,
|
|
"/resources/some-id",
|
|
json={
|
|
"title": "Bad",
|
|
"url": "http://169.254.169.254/latest",
|
|
},
|
|
content_type="application/json",
|
|
)
|
|
assert resp.status_code == 400
|
|
data = resp.get_json()
|
|
assert data["message"] == "Invalid URL"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# api_add_resource: success path (lines 227-245)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestApiAddResourceSuccess:
|
|
"""Success and exception paths in api_add_resource."""
|
|
|
|
def test_add_resource_success(self, app):
|
|
"""When research exists and URL valid, add resource and return success."""
|
|
mock_session = MagicMock()
|
|
mock_session.query.return_value = _build_filter_chain(MagicMock())
|
|
|
|
with patch(AUTH_DB_MANAGER, _mock_auth()):
|
|
with patch(
|
|
"local_deep_research.security.ssrf_validator.validate_url",
|
|
return_value=True,
|
|
):
|
|
with patch(
|
|
f"{MODULE}.get_user_db_session",
|
|
return_value=_make_db_ctx(mock_session),
|
|
):
|
|
with patch(
|
|
f"{MODULE}.add_resource", return_value=42
|
|
) as mock_add:
|
|
resp = _authed_post(
|
|
app,
|
|
"/resources/res-123",
|
|
json={
|
|
"title": "My Resource",
|
|
"url": "https://example.com/page",
|
|
"content_preview": "preview text",
|
|
"source_type": "pdf",
|
|
"metadata": {"key": "val"},
|
|
},
|
|
content_type="application/json",
|
|
)
|
|
|
|
assert resp.status_code == 200
|
|
data = resp.get_json()
|
|
assert data["status"] == "success"
|
|
assert data["resource_id"] == 42
|
|
mock_add.assert_called_once_with(
|
|
research_id="res-123",
|
|
title="My Resource",
|
|
url="https://example.com/page",
|
|
content_preview="preview text",
|
|
source_type="pdf",
|
|
metadata={"key": "val"},
|
|
)
|
|
|
|
def test_add_resource_exception_returns_500(self, app):
|
|
"""When add_resource raises, return 500."""
|
|
mock_session = MagicMock()
|
|
mock_session.query.return_value = _build_filter_chain(MagicMock())
|
|
|
|
with patch(AUTH_DB_MANAGER, _mock_auth()):
|
|
with patch(
|
|
"local_deep_research.security.ssrf_validator.validate_url",
|
|
return_value=True,
|
|
):
|
|
with patch(
|
|
f"{MODULE}.get_user_db_session",
|
|
return_value=_make_db_ctx(mock_session),
|
|
):
|
|
with patch(
|
|
f"{MODULE}.add_resource",
|
|
side_effect=RuntimeError("insert fail"),
|
|
):
|
|
resp = _authed_post(
|
|
app,
|
|
"/resources/res-123",
|
|
json={
|
|
"title": "My Resource",
|
|
"url": "https://example.com/page",
|
|
},
|
|
content_type="application/json",
|
|
)
|
|
|
|
assert resp.status_code == 500
|
|
data = resp.get_json()
|
|
assert data["status"] == "error"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# api_delete_resource: exception path (lines 274-276)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestApiDeleteResourceException:
|
|
"""Exception path in api_delete_resource."""
|
|
|
|
def test_delete_resource_exception_returns_500(self, app):
|
|
"""When delete_resource raises, return 500."""
|
|
with patch(AUTH_DB_MANAGER, _mock_auth()):
|
|
with patch(
|
|
f"{MODULE}.delete_resource",
|
|
side_effect=RuntimeError("delete boom"),
|
|
):
|
|
resp = _authed_delete(app, "/resources/res-id/delete/1")
|
|
assert resp.status_code == 500
|
|
data = resp.get_json()
|
|
assert data["status"] == "error"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# check_ollama_status: edge cases (lines 342-406)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestCheckOllamaStatusEdgeCases:
|
|
"""Edge cases for check_ollama_status endpoint."""
|
|
|
|
def test_old_api_format(self, app):
|
|
"""When response has no 'models' key, use old format (array)."""
|
|
app.config["LLM_CONFIG"] = {
|
|
"provider": "ollama",
|
|
"ollama_base_url": "http://localhost:11434",
|
|
}
|
|
mock_resp = MagicMock()
|
|
mock_resp.status_code = 200
|
|
mock_resp.json.return_value = [{"name": "llama3"}, {"name": "mistral"}]
|
|
|
|
with patch(AUTH_DB_MANAGER, _mock_auth()):
|
|
with patch(f"{MODULE}.safe_get", return_value=mock_resp):
|
|
resp = _authed_get(app, "/check/ollama_status")
|
|
|
|
assert resp.status_code == 200
|
|
data = resp.get_json()
|
|
assert data["running"] is True
|
|
assert data["model_count"] == 2
|
|
|
|
def test_invalid_json_response(self, app):
|
|
"""When response.json() raises ValueError, report running with warning."""
|
|
app.config["LLM_CONFIG"] = {
|
|
"provider": "ollama",
|
|
"ollama_base_url": "http://localhost:11434",
|
|
}
|
|
mock_resp = MagicMock()
|
|
mock_resp.status_code = 200
|
|
mock_resp.json.side_effect = ValueError("bad json")
|
|
|
|
with patch(AUTH_DB_MANAGER, _mock_auth()):
|
|
with patch(f"{MODULE}.safe_get", return_value=mock_resp):
|
|
resp = _authed_get(app, "/check/ollama_status")
|
|
|
|
assert resp.status_code == 200
|
|
data = resp.get_json()
|
|
assert data["running"] is True
|
|
assert "invalid" in data["message"].lower()
|
|
|
|
def test_non_200_status(self, app):
|
|
"""When Ollama returns non-200, report not running."""
|
|
app.config["LLM_CONFIG"] = {
|
|
"provider": "ollama",
|
|
"ollama_base_url": "http://localhost:11434",
|
|
}
|
|
mock_resp = MagicMock()
|
|
mock_resp.status_code = 503
|
|
|
|
with patch(AUTH_DB_MANAGER, _mock_auth()):
|
|
with patch(f"{MODULE}.safe_get", return_value=mock_resp):
|
|
resp = _authed_get(app, "/check/ollama_status")
|
|
|
|
assert resp.status_code == 200
|
|
data = resp.get_json()
|
|
assert data["running"] is False
|
|
assert data["status_code"] == 503
|
|
|
|
def test_timeout_error(self, app):
|
|
"""When safe_get raises Timeout, report not running with timeout type."""
|
|
app.config["LLM_CONFIG"] = {
|
|
"provider": "ollama",
|
|
"ollama_base_url": "http://localhost:11434",
|
|
}
|
|
|
|
with patch(AUTH_DB_MANAGER, _mock_auth()):
|
|
with patch(
|
|
f"{MODULE}.safe_get",
|
|
side_effect=requests.exceptions.Timeout("timed out"),
|
|
):
|
|
resp = _authed_get(app, "/check/ollama_status")
|
|
|
|
assert resp.status_code == 200
|
|
data = resp.get_json()
|
|
assert data["running"] is False
|
|
assert data["error_type"] == "timeout"
|
|
|
|
def test_general_exception(self, app):
|
|
"""When an unexpected exception occurs, report not running."""
|
|
app.config["LLM_CONFIG"] = {
|
|
"provider": "ollama",
|
|
"ollama_base_url": "http://localhost:11434",
|
|
}
|
|
|
|
with patch(AUTH_DB_MANAGER, _mock_auth()):
|
|
with patch(
|
|
f"{MODULE}.normalize_url",
|
|
side_effect=RuntimeError("unexpected"),
|
|
):
|
|
resp = _authed_get(app, "/check/ollama_status")
|
|
|
|
assert resp.status_code == 200
|
|
data = resp.get_json()
|
|
assert data["running"] is False
|
|
assert data["error_type"] == "exception"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# check_ollama_model: edge cases (lines 460-596)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestCheckOllamaModelEdgeCases:
|
|
"""Edge cases for check_ollama_model endpoint."""
|
|
|
|
def test_non_200_status(self, app):
|
|
"""When Ollama API returns non-200, report not available."""
|
|
app.config["LLM_CONFIG"] = {
|
|
"provider": "ollama",
|
|
"model": "llama3",
|
|
"ollama_base_url": "http://localhost:11434",
|
|
}
|
|
mock_resp = MagicMock()
|
|
mock_resp.status_code = 503
|
|
|
|
with patch(AUTH_DB_MANAGER, _mock_auth()):
|
|
with patch(f"{MODULE}.safe_get", return_value=mock_resp):
|
|
resp = _authed_get(app, "/check/ollama_model")
|
|
|
|
assert resp.status_code == 200
|
|
data = resp.get_json()
|
|
assert data["available"] is False
|
|
assert data["status_code"] == 503
|
|
|
|
def test_old_api_format(self, app):
|
|
"""When response has no 'models' key, use old format (array)."""
|
|
app.config["LLM_CONFIG"] = {
|
|
"provider": "ollama",
|
|
"model": "llama3",
|
|
"ollama_base_url": "http://localhost:11434",
|
|
}
|
|
mock_resp = MagicMock()
|
|
mock_resp.status_code = 200
|
|
mock_resp.json.return_value = [{"name": "llama3"}]
|
|
|
|
with patch(AUTH_DB_MANAGER, _mock_auth()):
|
|
with patch(f"{MODULE}.safe_get", return_value=mock_resp):
|
|
resp = _authed_get(app, "/check/ollama_model")
|
|
|
|
assert resp.status_code == 200
|
|
data = resp.get_json()
|
|
assert data["available"] is True
|
|
|
|
def test_no_models_found(self, app):
|
|
"""When models list is empty, report not available with pull message."""
|
|
app.config["LLM_CONFIG"] = {
|
|
"provider": "ollama",
|
|
"model": "llama3",
|
|
"ollama_base_url": "http://localhost:11434",
|
|
}
|
|
mock_resp = MagicMock()
|
|
mock_resp.status_code = 200
|
|
mock_resp.json.return_value = {"models": []}
|
|
|
|
with patch(AUTH_DB_MANAGER, _mock_auth()):
|
|
with patch(f"{MODULE}.safe_get", return_value=mock_resp):
|
|
resp = _authed_get(app, "/check/ollama_model")
|
|
|
|
assert resp.status_code == 200
|
|
data = resp.get_json()
|
|
assert data["available"] is False
|
|
assert "pull" in data["message"].lower()
|
|
|
|
def test_json_parse_error(self, app):
|
|
"""When response.json() raises ValueError, report parse error."""
|
|
app.config["LLM_CONFIG"] = {
|
|
"provider": "ollama",
|
|
"model": "llama3",
|
|
"ollama_base_url": "http://localhost:11434",
|
|
}
|
|
mock_resp = MagicMock()
|
|
mock_resp.status_code = 200
|
|
mock_resp.json.side_effect = ValueError("bad json")
|
|
|
|
with patch(AUTH_DB_MANAGER, _mock_auth()):
|
|
with patch(f"{MODULE}.safe_get", return_value=mock_resp):
|
|
resp = _authed_get(app, "/check/ollama_model")
|
|
|
|
assert resp.status_code == 200
|
|
data = resp.get_json()
|
|
assert data["available"] is False
|
|
assert data["error_type"] == "json_parse_error"
|
|
|
|
def test_connection_error(self, app):
|
|
"""When safe_get raises ConnectionError, report connection error."""
|
|
app.config["LLM_CONFIG"] = {
|
|
"provider": "ollama",
|
|
"model": "llama3",
|
|
"ollama_base_url": "http://localhost:11434",
|
|
}
|
|
|
|
with patch(AUTH_DB_MANAGER, _mock_auth()):
|
|
with patch(
|
|
f"{MODULE}.safe_get",
|
|
side_effect=requests.exceptions.ConnectionError("refused"),
|
|
):
|
|
resp = _authed_get(app, "/check/ollama_model")
|
|
|
|
assert resp.status_code == 200
|
|
data = resp.get_json()
|
|
assert data["available"] is False
|
|
assert data["error_type"] == "connection_error"
|
|
|
|
def test_timeout_error(self, app):
|
|
"""When safe_get raises Timeout, report timeout."""
|
|
app.config["LLM_CONFIG"] = {
|
|
"provider": "ollama",
|
|
"model": "llama3",
|
|
"ollama_base_url": "http://localhost:11434",
|
|
}
|
|
|
|
with patch(AUTH_DB_MANAGER, _mock_auth()):
|
|
with patch(
|
|
f"{MODULE}.safe_get",
|
|
side_effect=requests.exceptions.Timeout("timed out"),
|
|
):
|
|
resp = _authed_get(app, "/check/ollama_model")
|
|
|
|
assert resp.status_code == 200
|
|
data = resp.get_json()
|
|
assert data["available"] is False
|
|
assert data["error_type"] == "timeout"
|
|
|
|
def test_general_exception(self, app):
|
|
"""When an unexpected exception occurs, report exception."""
|
|
app.config["LLM_CONFIG"] = {
|
|
"provider": "ollama",
|
|
"model": "llama3",
|
|
"ollama_base_url": "http://localhost:11434",
|
|
}
|
|
|
|
with patch(AUTH_DB_MANAGER, _mock_auth()):
|
|
with patch(
|
|
f"{MODULE}.normalize_url",
|
|
side_effect=RuntimeError("unexpected"),
|
|
):
|
|
resp = _authed_get(app, "/check/ollama_model")
|
|
|
|
assert resp.status_code == 200
|
|
data = resp.get_json()
|
|
assert data["available"] is False
|
|
assert data["error_type"] == "exception"
|
|
|
|
|
|
class TestProbeOllamaTags:
|
|
"""Direct tests for the shared _probe_ollama_tags helper — the single
|
|
source the status and model-availability checks both consume, so an
|
|
'is Ollama up?' answer can no longer drift between them."""
|
|
|
|
def _probe(self, base_url="http://localhost:11434"):
|
|
from local_deep_research.web.routes.api_routes import (
|
|
_probe_ollama_tags,
|
|
)
|
|
|
|
return _probe_ollama_tags(base_url)
|
|
|
|
def test_ok_new_format(self):
|
|
resp = MagicMock()
|
|
resp.status_code = 200
|
|
resp.json.return_value = {"models": [{"name": "llama3"}]}
|
|
with patch(f"{MODULE}.safe_get", return_value=resp):
|
|
outcome, payload = self._probe()
|
|
assert outcome == "ok"
|
|
assert payload == [{"name": "llama3"}]
|
|
|
|
def test_ok_old_format(self):
|
|
resp = MagicMock()
|
|
resp.status_code = 200
|
|
resp.json.return_value = [{"name": "a"}, {"name": "b"}]
|
|
with patch(f"{MODULE}.safe_get", return_value=resp):
|
|
outcome, payload = self._probe()
|
|
assert outcome == "ok"
|
|
assert len(payload) == 2
|
|
|
|
def test_bad_status_returns_status_code(self):
|
|
resp = MagicMock()
|
|
resp.status_code = 503
|
|
with patch(f"{MODULE}.safe_get", return_value=resp):
|
|
outcome, payload = self._probe()
|
|
assert outcome == "bad_status"
|
|
assert payload == 503
|
|
|
|
def test_invalid_json(self):
|
|
resp = MagicMock()
|
|
resp.status_code = 200
|
|
resp.json.side_effect = ValueError("bad")
|
|
with patch(f"{MODULE}.safe_get", return_value=resp):
|
|
outcome, payload = self._probe()
|
|
assert outcome == "invalid_json"
|
|
assert payload is None
|
|
|
|
def test_connection_error(self):
|
|
import requests
|
|
|
|
with patch(
|
|
f"{MODULE}.safe_get",
|
|
side_effect=requests.exceptions.ConnectionError(),
|
|
):
|
|
outcome, payload = self._probe()
|
|
assert outcome == "connection_error"
|
|
|
|
def test_timeout(self):
|
|
import requests
|
|
|
|
with patch(
|
|
f"{MODULE}.safe_get", side_effect=requests.exceptions.Timeout()
|
|
):
|
|
outcome, payload = self._probe()
|
|
assert outcome == "timeout"
|