Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

491 lines
18 KiB
Python

# allow: no-sut-import — black-box HTTP test; drives real routes through the Flask test client
"""Tests for api_routes module - API endpoints."""
from unittest.mock import patch, MagicMock
# API routes are registered under /research/api prefix
API_PREFIX = "/research/api"
class TestGetCurrentConfig:
"""Tests for /settings/current-config endpoint."""
def test_requires_authentication(self, client):
"""Should require authentication."""
response = client.get(f"{API_PREFIX}/settings/current-config")
# Should redirect to login or return 401
assert response.status_code == 401, response.status_code
def test_returns_config_when_authenticated(self, authenticated_client):
"""Should return config when authenticated."""
with patch(
"local_deep_research.web.routes.api_routes.get_user_db_session"
) as mock_session_ctx:
mock_session = MagicMock()
mock_session_ctx.return_value.__enter__ = MagicMock(
return_value=mock_session
)
mock_session_ctx.return_value.__exit__ = MagicMock(
return_value=None
)
with patch(
"local_deep_research.web.routes.api_routes.SettingsManager"
) as mock_sm:
mock_instance = MagicMock()
mock_instance.get_setting.side_effect = lambda key, default: {
"llm.provider": "ollama",
"llm.model": "llama3",
"search.tool": "searxng",
"search.iterations": 8,
"search.questions_per_iteration": 5,
"search.search_strategy": "focused_iteration",
}.get(key, default)
mock_sm.return_value = mock_instance
response = authenticated_client.get(
f"{API_PREFIX}/settings/current-config"
)
assert response.status_code == 200
data = response.get_json()
assert data["success"] is True
assert "config" in data
assert data["config"]["provider"] == "ollama"
class TestApiStartResearch:
"""Tests for /start endpoint."""
def test_requires_authentication(self, client):
"""Should require authentication."""
response = client.post(
f"{API_PREFIX}/start", json={"query": "test query", "mode": "quick"}
)
assert response.status_code == 401, response.status_code
def test_requires_query(self, authenticated_client):
"""Should require query parameter."""
response = authenticated_client.post(
f"{API_PREFIX}/start", json={"mode": "quick"}
)
assert response.status_code == 400
data = response.get_json()
assert "Query is required" in data.get("message", "")
def test_empty_query_rejected(self, authenticated_client):
"""Should reject empty query."""
response = authenticated_client.post(
f"{API_PREFIX}/start", json={"query": "", "mode": "quick"}
)
assert response.status_code == 400
def test_starts_research_successfully(self, authenticated_client):
"""Should delegate to start_research and return success."""
with patch(
"local_deep_research.web.routes.research_routes.start_research"
) as mock_start:
mock_start.return_value = (
authenticated_client.application.make_response(
(
'{"status": "success", "research_id": "test-uuid"}',
200,
{"Content-Type": "application/json"},
)
)
)
response = authenticated_client.post(
f"{API_PREFIX}/start",
json={"query": "What is AI?", "mode": "quick"},
)
assert response.status_code == 200
data = response.get_json()
assert data["status"] == "success"
assert data["research_id"] == "test-uuid"
mock_start.assert_called_once()
def test_starts_research_queued(self, authenticated_client):
"""Should delegate to start_research and return queued status."""
with patch(
"local_deep_research.web.routes.research_routes.start_research"
) as mock_start:
mock_start.return_value = authenticated_client.application.make_response(
(
'{"status": "queued", "research_id": "test-uuid", "queue_position": 1}',
200,
{"Content-Type": "application/json"},
)
)
response = authenticated_client.post(
f"{API_PREFIX}/start",
json={"query": "What is AI?", "mode": "quick"},
)
assert response.status_code == 200
data = response.get_json()
assert data["status"] == "queued"
assert data["research_id"] == "test-uuid"
mock_start.assert_called_once()
class TestApiResearchStatus:
"""Tests for /status/<research_id> endpoint."""
def test_requires_authentication(self, client):
"""Should require authentication."""
response = client.get(f"{API_PREFIX}/status/test-id")
assert response.status_code == 401, response.status_code
def test_returns_404_for_nonexistent(self, authenticated_client):
"""Should return 404 for non-existent research."""
with patch(
"local_deep_research.web.routes.api_routes.get_user_db_session"
) as mock_session_ctx:
mock_session = MagicMock()
mock_session_ctx.return_value.__enter__ = MagicMock(
return_value=mock_session
)
mock_session_ctx.return_value.__exit__ = MagicMock(
return_value=None
)
mock_query = MagicMock()
mock_query.filter_by.return_value.first.return_value = None
mock_session.query.return_value = mock_query
response = authenticated_client.get(
f"{API_PREFIX}/status/nonexistent-id"
)
assert response.status_code == 404
def test_returns_status_for_existing(self, authenticated_client):
"""Should return status for existing research."""
with patch(
"local_deep_research.web.routes.api_routes.get_user_db_session"
) as mock_session_ctx:
mock_session = MagicMock()
mock_session_ctx.return_value.__enter__ = MagicMock(
return_value=mock_session
)
mock_session_ctx.return_value.__exit__ = MagicMock(
return_value=None
)
mock_research = MagicMock()
mock_research.status = "completed"
mock_research.progress = 100
mock_research.completed_at = "2024-01-01T00:00:00"
mock_research.report_path = "/path/to/report.md"
mock_research.research_meta = {"key": "value"}
mock_query = MagicMock()
mock_query.filter_by.return_value.first.return_value = mock_research
mock_session.query.return_value = mock_query
response = authenticated_client.get(
f"{API_PREFIX}/status/existing-id"
)
assert response.status_code == 200
data = response.get_json()
assert data["status"] == "completed"
assert data["progress"] == 100
class TestApiTerminateResearch:
"""Tests for /terminate/<research_id> endpoint."""
def test_requires_authentication(self, client):
"""Should require authentication."""
response = client.post(f"{API_PREFIX}/terminate/test-id")
assert response.status_code == 401, response.status_code
def test_terminates_research(self, authenticated_client):
"""Should terminate research."""
with patch(
"local_deep_research.web.routes.api_routes.cancel_research"
) as mock_cancel:
mock_cancel.return_value = True
response = authenticated_client.post(
f"{API_PREFIX}/terminate/test-id"
)
assert response.status_code == 200
data = response.get_json()
assert data["status"] == "success"
def test_handles_not_found(self, authenticated_client):
"""Should handle research not found."""
with patch(
"local_deep_research.web.routes.api_routes.cancel_research"
) as mock_cancel:
mock_cancel.return_value = False
response = authenticated_client.post(
f"{API_PREFIX}/terminate/nonexistent"
)
assert response.status_code == 200
data = response.get_json()
assert "not found or already completed" in data["message"]
class TestApiGetResources:
"""Tests for GET /resources/<research_id> endpoint."""
def test_requires_authentication(self, client):
"""Should require authentication."""
response = client.get(f"{API_PREFIX}/resources/test-id")
assert response.status_code == 401, response.status_code
def test_returns_resources(self, authenticated_client):
"""Should return resources for research."""
with patch(
"local_deep_research.web.routes.api_routes.get_resources_for_research"
) as mock_get:
mock_get.return_value = [
{"id": 1, "title": "Resource 1", "url": "https://example.com"}
]
response = authenticated_client.get(
f"{API_PREFIX}/resources/test-id"
)
assert response.status_code == 200
data = response.get_json()
assert data["status"] == "success"
assert len(data["resources"]) == 1
class TestApiAddResource:
"""Tests for POST /resources/<research_id> endpoint."""
def test_requires_authentication(self, client):
"""Should require authentication."""
response = client.post(
f"{API_PREFIX}/resources/test-id",
json={"title": "Test", "url": "https://example.com"},
)
assert response.status_code == 401, response.status_code
def test_requires_title_and_url(self, authenticated_client):
"""Should require both title and URL."""
response = authenticated_client.post(
f"{API_PREFIX}/resources/test-id", json={"title": "Test only"}
)
assert response.status_code == 400
response = authenticated_client.post(
f"{API_PREFIX}/resources/test-id",
json={"url": "https://example.com"},
)
assert response.status_code == 400
def test_returns_404_for_nonexistent_research(self, authenticated_client):
"""Should return 404 if research doesn't exist."""
with (
patch(
"local_deep_research.web.routes.api_routes.get_user_db_session"
) as mock_session_ctx,
patch(
"local_deep_research.security.ssrf_validator.validate_url",
return_value=(True, None),
),
):
mock_session = MagicMock()
mock_session_ctx.return_value.__enter__ = MagicMock(
return_value=mock_session
)
mock_session_ctx.return_value.__exit__ = MagicMock(
return_value=None
)
mock_query = MagicMock()
mock_query.filter_by.return_value.first.return_value = None
mock_session.query.return_value = mock_query
response = authenticated_client.post(
f"{API_PREFIX}/resources/nonexistent",
json={"title": "Test", "url": "https://example.com"},
)
assert response.status_code == 404
class TestApiDeleteResource:
"""Tests for DELETE /resources/<research_id>/delete/<resource_id> endpoint."""
def test_requires_authentication(self, client):
"""Should require authentication."""
response = client.delete(f"{API_PREFIX}/resources/test-id/delete/1")
assert response.status_code == 401, response.status_code
def test_deletes_resource(self, authenticated_client):
"""Should delete resource successfully."""
with patch(
"local_deep_research.web.routes.api_routes.delete_resource"
) as mock_delete:
mock_delete.return_value = True
response = authenticated_client.delete(
f"{API_PREFIX}/resources/test-id/delete/1"
)
assert response.status_code == 200
data = response.get_json()
assert data["status"] == "success"
def test_returns_404_for_nonexistent(self, authenticated_client):
"""Should return 404 for non-existent resource."""
with patch(
"local_deep_research.web.routes.api_routes.delete_resource"
) as mock_delete:
mock_delete.return_value = False
response = authenticated_client.delete(
f"{API_PREFIX}/resources/test-id/delete/999"
)
assert response.status_code == 404
class TestCheckOllamaStatus:
"""Tests for /check/ollama_status endpoint."""
def test_requires_authentication(self, client):
"""Should require authentication."""
response = client.get(f"{API_PREFIX}/check/ollama_status")
assert response.status_code == 401, response.status_code
def test_non_ollama_provider(self, authenticated_client, app):
"""Should return running=True for non-Ollama providers."""
app.config["LLM_CONFIG"] = {"provider": "openai"}
response = authenticated_client.get(f"{API_PREFIX}/check/ollama_status")
assert response.status_code == 200
data = response.get_json()
assert data["running"] is True
assert "openai" in data["message"].lower()
def test_ollama_connection_error(self, authenticated_client, app):
"""Should return running=False on connection error."""
import requests
app.config["LLM_CONFIG"] = {
"provider": "ollama",
"ollama_base_url": "http://localhost:11434",
}
with patch(
"local_deep_research.web.routes.api_routes.safe_get",
side_effect=requests.exceptions.ConnectionError(
"Connection refused"
),
):
response = authenticated_client.get(
f"{API_PREFIX}/check/ollama_status"
)
assert response.status_code == 200
data = response.get_json()
assert data["running"] is False
assert "error_type" in data
def test_ollama_running(self, authenticated_client, app):
"""Should return running=True when Ollama is running."""
app.config["LLM_CONFIG"] = {
"provider": "ollama",
"ollama_base_url": "http://localhost:11434",
}
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {"models": [{"name": "llama3"}]}
with patch(
"local_deep_research.web.routes.api_routes.safe_get",
return_value=mock_response,
):
response = authenticated_client.get(
f"{API_PREFIX}/check/ollama_status"
)
assert response.status_code == 200
data = response.get_json()
assert data["running"] is True
assert data["model_count"] == 1
class TestCheckOllamaModel:
"""Tests for /check/ollama_model endpoint."""
def test_requires_authentication(self, client):
"""Should require authentication."""
response = client.get(f"{API_PREFIX}/check/ollama_model")
assert response.status_code == 401, response.status_code
def test_non_ollama_provider(self, authenticated_client, app):
"""Should return available=True for non-Ollama providers."""
app.config["LLM_CONFIG"] = {"provider": "openai"}
response = authenticated_client.get(f"{API_PREFIX}/check/ollama_model")
assert response.status_code == 200
data = response.get_json()
assert data["available"] is True
assert data["provider"] == "openai"
def test_model_available(self, authenticated_client, app):
"""Should return available=True when model exists."""
app.config["LLM_CONFIG"] = {
"provider": "ollama",
"model": "llama3",
"ollama_base_url": "http://localhost:11434",
}
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {"models": [{"name": "llama3"}]}
with patch(
"local_deep_research.web.routes.api_routes.safe_get",
return_value=mock_response,
):
response = authenticated_client.get(
f"{API_PREFIX}/check/ollama_model"
)
assert response.status_code == 200
data = response.get_json()
assert data["available"] is True
assert data["model"] == "llama3"
def test_model_not_available(self, authenticated_client, app):
"""Should return available=False when model doesn't exist."""
app.config["LLM_CONFIG"] = {
"provider": "ollama",
"model": "nonexistent-model",
"ollama_base_url": "http://localhost:11434",
}
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {"models": [{"name": "llama3"}]}
with patch(
"local_deep_research.web.routes.api_routes.safe_get",
return_value=mock_response,
):
response = authenticated_client.get(
f"{API_PREFIX}/check/ollama_model"
)
assert response.status_code == 200
data = response.get_json()
assert data["available"] is False