项目文件夹

文件
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

790 行
28 KiB
Python

"""
Extra coverage tests for local_deep_research.news.flask_api.
Targets uncovered/under-tested branches:
- safe_error_message: context='' produces generic message without 'while'
- _is_job_owned_by_user: no args attr, empty args, no user_sessions, system job
- get_news_feed: error-in-result with 'must be between' yields 400 vs generic 500
- create_subscription: get_json(force=True) raises (invalid JSON body)
- vote_on_news: ValueError with 'not found' yields 404
- submit_feedback: ValueError with 'must be' yields 400; generic ValueError yields 400
- get_subscription: null/undefined ID returns 400; valid ID returns data; exception 500
- update_subscription: api returns error with 'not found' -> 404 vs other -> 400
- delete_subscription: success path; not-found path
- run_subscription_now: subscription not found; response.ok but non-success status; response not ok
- stop_scheduler: not running; no scheduler instance
- check_subscriptions_now: no scheduler; scheduler not running
- trigger_cleanup: scheduler not running
- get_active_users: show_all=False filters to current user only
- scheduler_stats: show_all=False scopes; last_activity=None
- get_search_history: unauthenticated returns empty
- add_search_history: unauthenticated returns 401
- clear_search_history: unauthenticated returns success
- get_scheduler_status: APScheduler get_jobs raises exception
- update_subscription_folder: not found; refresh_interval recalc with/without last_refresh
- check_overdue_subscriptions: inner exception per-subscription
"""
from unittest.mock import MagicMock, patch
import pytest
from flask import Flask
MODULE = "local_deep_research.news.flask_api"
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_app():
"""Create a minimal Flask app with only the news_api blueprint."""
app = Flask(__name__)
app.config["SECRET_KEY"] = "test-secret"
app.config["WTF_CSRF_ENABLED"] = False
app.config["TESTING"] = True
from local_deep_research.news.flask_api import news_api_bp
app.register_blueprint(news_api_bp, url_prefix="/news/api")
return app
def _auth_session(client, username="testuser"):
"""Inject an authenticated session."""
with client.session_transaction() as sess:
sess["username"] = username
def _common_patches():
"""Return dict of context-manager patches for auth + db plumbing."""
return {
"db_manager": patch(
"local_deep_research.web.auth.decorators.db_manager"
),
"get_user_id": patch(f"{MODULE}.get_user_id", return_value="testuser"),
"get_settings_manager": patch(f"{MODULE}.get_settings_manager"),
}
@pytest.fixture
def app():
"""Flask app fixture."""
return _make_app()
@pytest.fixture
def client(app):
"""Test client fixture."""
return app.test_client()
@pytest.fixture
def authed_client(client):
"""Test client with authenticated session."""
_auth_session(client)
return client
# ---------------------------------------------------------------------------
# safe_error_message
# ---------------------------------------------------------------------------
class TestSafeErrorMessageExtraCoverage:
"""Edge cases for safe_error_message."""
def test_generic_error_empty_context(self):
"""Empty context string produces message without 'while'."""
from local_deep_research.news.flask_api import safe_error_message
result = safe_error_message(RuntimeError("boom"), context="")
assert result == "An error occurred"
assert "while" not in result
def test_generic_error_with_context(self):
"""Non-empty context string includes 'while <context>'."""
from local_deep_research.news.flask_api import safe_error_message
result = safe_error_message(RuntimeError("boom"), context="loading")
assert "while loading" in result
# ---------------------------------------------------------------------------
# _is_job_owned_by_user
# ---------------------------------------------------------------------------
class TestIsJobOwnedByUser:
"""Branch coverage for _is_job_owned_by_user."""
def test_no_args_attribute(self):
"""Job without args attribute falls through to scheduler check."""
from local_deep_research.news.flask_api import _is_job_owned_by_user
job = MagicMock(spec=[]) # no .args
scheduler = MagicMock(spec=[]) # no .user_sessions
assert _is_job_owned_by_user(job, "alice", scheduler) is False
def test_empty_args_list(self):
"""Job with empty args list does not match."""
from local_deep_research.news.flask_api import _is_job_owned_by_user
job = MagicMock()
job.args = []
scheduler = MagicMock(spec=[])
assert _is_job_owned_by_user(job, "alice", scheduler) is False
def test_args_first_element_mismatch(self):
"""Job args[0] does not equal username; scheduler has no user_sessions."""
from local_deep_research.news.flask_api import _is_job_owned_by_user
job = MagicMock()
job.args = ["bob"]
scheduler = MagicMock(spec=[])
assert _is_job_owned_by_user(job, "alice", scheduler) is False
def test_fallback_scheduled_jobs_match(self):
"""Job matched via scheduler.user_sessions scheduled_jobs set."""
from local_deep_research.news.flask_api import _is_job_owned_by_user
job = MagicMock()
job.args = ["other_user"]
job.id = "job-42"
scheduler = MagicMock()
scheduler.user_sessions = {
"alice": {"scheduled_jobs": {"job-42", "job-99"}}
}
assert _is_job_owned_by_user(job, "alice", scheduler) is True
# ---------------------------------------------------------------------------
# GET /feed – error-in-result branches
# ---------------------------------------------------------------------------
class TestGetNewsFeedErrorBranches:
"""Covers the error-in-result branching in get_news_feed."""
def test_error_with_must_be_between_returns_400(self, authed_client):
"""When api result error contains 'must be between', status is 400."""
cp = _common_patches()
error_result = {
"error": "limit must be between 1 and 100",
"news_items": [],
}
with (
cp["db_manager"] as mdb,
cp["get_user_id"],
cp["get_settings_manager"] as msm,
patch(f"{MODULE}.api.get_news_feed", return_value=error_result),
):
mdb.is_user_connected.return_value = True
sm = MagicMock()
sm.get_setting.return_value = 20
msm.return_value = sm
resp = authed_client.get("/news/api/feed")
assert resp.status_code == 400
def test_error_without_must_be_between_returns_500(self, authed_client):
"""When api result error does NOT contain 'must be between', status is 500."""
cp = _common_patches()
error_result = {
"error": "database connection failed",
"news_items": [],
}
with (
cp["db_manager"] as mdb,
cp["get_user_id"],
cp["get_settings_manager"] as msm,
patch(f"{MODULE}.api.get_news_feed", return_value=error_result),
):
mdb.is_user_connected.return_value = True
sm = MagicMock()
sm.get_setting.return_value = 20
msm.return_value = sm
resp = authed_client.get("/news/api/feed")
assert resp.status_code == 500
# ---------------------------------------------------------------------------
# POST /subscribe – invalid JSON
# ---------------------------------------------------------------------------
class TestCreateSubscriptionInvalidJson:
"""Covers the get_json(force=True) exception branch."""
def test_invalid_json_body_returns_400(self, authed_client):
"""Malformed JSON body returns 400."""
cp = _common_patches()
with cp["db_manager"] as mdb, cp["get_user_id"]:
mdb.is_user_connected.return_value = True
resp = authed_client.post(
"/news/api/subscribe",
data="not-json{{{",
content_type="application/json",
)
assert resp.status_code == 400
# ---------------------------------------------------------------------------
# POST /vote – ValueError branches
# ---------------------------------------------------------------------------
class TestVoteOnNewsValueErrorBranches:
"""Covers ValueError handling in vote_on_news."""
def test_value_error_not_found_returns_404(self, authed_client):
"""ValueError with 'not found' yields 404."""
cp = _common_patches()
with (
cp["db_manager"] as mdb,
cp["get_user_id"],
patch(
f"{MODULE}.api.submit_feedback",
side_effect=ValueError("Card not found"),
),
):
mdb.is_user_connected.return_value = True
resp = authed_client.post(
"/news/api/vote",
json={"card_id": "c1", "vote": "up"},
)
assert resp.status_code == 404
assert "not found" in resp.get_json()["error"].lower()
def test_value_error_other_returns_400(self, authed_client):
"""ValueError without 'not found' yields 400."""
cp = _common_patches()
with (
cp["db_manager"] as mdb,
cp["get_user_id"],
patch(
f"{MODULE}.api.submit_feedback",
side_effect=ValueError("vote must be up or down"),
),
):
mdb.is_user_connected.return_value = True
resp = authed_client.post(
"/news/api/vote",
json={"card_id": "c1", "vote": "invalid"},
)
assert resp.status_code == 400
# ---------------------------------------------------------------------------
# POST /feedback/<card_id> – ValueError 'must be' branch
# ---------------------------------------------------------------------------
class TestSubmitFeedbackValueErrors:
"""Covers ValueError branches in submit_feedback route."""
def test_must_be_error_returns_400(self, authed_client):
"""ValueError with 'must be' in message yields 400."""
cp = _common_patches()
with (
cp["db_manager"] as mdb,
cp["get_user_id"],
patch(
f"{MODULE}.api.submit_feedback",
side_effect=ValueError("vote must be 'up' or 'down'"),
),
):
mdb.is_user_connected.return_value = True
resp = authed_client.post(
"/news/api/feedback/card-1",
json={"vote": "sideways"},
)
assert resp.status_code == 400
assert "Invalid input value" in resp.get_json()["error"]
def test_not_found_error_returns_404(self, authed_client):
"""ValueError with 'not found' yields 404."""
cp = _common_patches()
with (
cp["db_manager"] as mdb,
cp["get_user_id"],
patch(
f"{MODULE}.api.submit_feedback",
side_effect=ValueError("Card not found in database"),
),
):
mdb.is_user_connected.return_value = True
resp = authed_client.post(
"/news/api/feedback/card-1",
json={"vote": "up"},
)
assert resp.status_code == 404
# ---------------------------------------------------------------------------
# PUT /subscriptions/<id> – update error branches
# ---------------------------------------------------------------------------
class TestUpdateSubscriptionErrorBranches:
"""Covers error-in-result branching in update_subscription."""
def test_api_error_not_found_returns_404(self, authed_client):
"""When api.update_subscription result has 'not found' error -> 404."""
cp = _common_patches()
with (
cp["db_manager"] as mdb,
cp["get_user_id"],
patch(
f"{MODULE}.api.update_subscription",
return_value={"error": "Subscription not found"},
),
):
mdb.is_user_connected.return_value = True
resp = authed_client.put(
"/news/api/subscriptions/sub-1",
json={"query": "new query"},
)
assert resp.status_code == 404
def test_api_error_other_returns_400(self, authed_client):
"""When api.update_subscription result has other error -> 400."""
cp = _common_patches()
with (
cp["db_manager"] as mdb,
cp["get_user_id"],
patch(
f"{MODULE}.api.update_subscription",
return_value={"error": "Invalid field value"},
),
):
mdb.is_user_connected.return_value = True
resp = authed_client.put(
"/news/api/subscriptions/sub-1",
json={"query": "new query"},
)
assert resp.status_code == 400
# ---------------------------------------------------------------------------
# POST /scheduler/stop – branches
# ---------------------------------------------------------------------------
class TestStopSchedulerBranches:
"""Covers stop_scheduler edge cases."""
def test_scheduler_not_running_returns_200(self, authed_client):
"""Scheduler exists but is not running."""
cp = _common_patches()
mock_scheduler = MagicMock()
mock_scheduler.is_running = False
with (
cp["db_manager"] as mdb,
patch(f"{MODULE}.get_env_setting", return_value=True),
):
mdb.is_user_connected.return_value = True
with authed_client.application.test_request_context():
authed_client.application.background_job_scheduler = (
mock_scheduler
)
resp = authed_client.post("/news/api/scheduler/stop")
assert resp.status_code == 200
data = resp.get_json()
assert "not running" in data.get("message", "").lower()
def test_no_scheduler_instance_returns_404(self, authed_client):
"""No news_scheduler attribute on app."""
cp = _common_patches()
with (
cp["db_manager"] as mdb,
patch(f"{MODULE}.get_env_setting", return_value=True),
):
mdb.is_user_connected.return_value = True
# Ensure no news_scheduler attribute
if hasattr(authed_client.application, "background_job_scheduler"):
delattr(authed_client.application, "background_job_scheduler")
resp = authed_client.post("/news/api/scheduler/stop")
assert resp.status_code == 404
data = resp.get_json()
assert "no scheduler" in data.get("message", "").lower()
# ---------------------------------------------------------------------------
# POST /scheduler/check-now – branches
# ---------------------------------------------------------------------------
class TestCheckSubscriptionsNowBranches:
"""Covers check_subscriptions_now edge cases."""
def test_no_scheduler_returns_503(self, authed_client):
"""No scheduler attribute on app returns 503."""
cp = _common_patches()
with (
cp["db_manager"] as mdb,
patch(f"{MODULE}.get_env_setting", return_value=True),
):
mdb.is_user_connected.return_value = True
if hasattr(authed_client.application, "background_job_scheduler"):
delattr(authed_client.application, "background_job_scheduler")
resp = authed_client.post("/news/api/scheduler/check-now")
assert resp.status_code == 503
def test_scheduler_not_running_returns_503(self, authed_client):
"""Scheduler exists but not running returns 503."""
cp = _common_patches()
mock_scheduler = MagicMock()
mock_scheduler.is_running = False
with (
cp["db_manager"] as mdb,
patch(f"{MODULE}.get_env_setting", return_value=True),
):
mdb.is_user_connected.return_value = True
authed_client.application.background_job_scheduler = mock_scheduler
resp = authed_client.post("/news/api/scheduler/check-now")
assert resp.status_code == 503
# ---------------------------------------------------------------------------
# POST /scheduler/cleanup-now – scheduler not running
# ---------------------------------------------------------------------------
class TestTriggerCleanupBranches:
"""Covers trigger_cleanup edge cases."""
def test_scheduler_not_running_returns_400(self, authed_client):
"""Scheduler not running returns 400."""
cp = _common_patches()
mock_scheduler = MagicMock()
mock_scheduler.is_running = False
with (
cp["db_manager"] as mdb,
patch(f"{MODULE}.get_env_setting", return_value=True),
patch(
f"{MODULE}.get_background_job_scheduler"
if False
else "local_deep_research.scheduler.background.get_background_job_scheduler",
return_value=mock_scheduler,
),
):
mdb.is_user_connected.return_value = True
resp = authed_client.post("/news/api/scheduler/cleanup-now")
assert resp.status_code == 400
assert "not running" in resp.get_json()["error"].lower()
# ---------------------------------------------------------------------------
# GET /scheduler/status – APScheduler get_jobs exception
# ---------------------------------------------------------------------------
class TestSchedulerStatusApschedulerException:
"""Covers the exception path when get_jobs raises."""
def test_apscheduler_exception_sets_count_zero(self, authed_client):
"""When scheduler.scheduler.get_jobs() raises, apscheduler_job_count=0."""
cp = _common_patches()
mock_scheduler = MagicMock()
mock_scheduler.is_running = True
mock_scheduler.config = {}
mock_scheduler.user_sessions = {"testuser": {"scheduled_jobs": set()}}
mock_scheduler.scheduler.get_jobs.side_effect = RuntimeError(
"lock error"
)
with (
cp["db_manager"] as mdb,
patch(
"local_deep_research.scheduler.background.get_background_job_scheduler",
return_value=mock_scheduler,
),
patch(f"{MODULE}.get_env_setting", return_value=True),
):
mdb.is_user_connected.return_value = True
resp = authed_client.get("/news/api/scheduler/status")
assert resp.status_code == 200
data = resp.get_json()
assert data["apscheduler_job_count"] == 0
# ---------------------------------------------------------------------------
# GET /scheduler/users – show_all=False filtering
# ---------------------------------------------------------------------------
class TestGetActiveUsersFiltering:
"""Covers user filtering when show_all=False."""
def test_only_current_user_returned(self, authed_client):
"""When show_all=False, only current user is returned."""
cp = _common_patches()
mock_scheduler = MagicMock()
mock_scheduler.get_user_sessions_summary.return_value = [
{"user_id": "testuser", "jobs": 2},
{"user_id": "otheruser", "jobs": 5},
]
with (
cp["db_manager"] as mdb,
patch(
"local_deep_research.scheduler.background.get_background_job_scheduler",
return_value=mock_scheduler,
),
patch(f"{MODULE}.get_env_setting", return_value=False),
):
mdb.is_user_connected.return_value = True
resp = authed_client.get("/news/api/scheduler/users")
assert resp.status_code == 200
data = resp.get_json()
assert data["active_users"] == 1
assert all(u["user_id"] == "testuser" for u in data["users"])
# ---------------------------------------------------------------------------
# GET /search-history – unauthenticated
# ---------------------------------------------------------------------------
class TestSearchHistoryUnauthenticated:
"""Covers unauthenticated paths in search-history endpoints."""
def test_get_history_no_user_returns_empty(self, authed_client):
"""When current_user returns None, empty history is returned."""
cp = _common_patches()
with (
cp["db_manager"] as mdb,
patch(f"{MODULE}.get_user_id", return_value=None),
patch(
"local_deep_research.web.auth.decorators.current_user",
return_value=None,
),
):
mdb.is_user_connected.return_value = True
resp = authed_client.get("/news/api/search-history")
assert resp.status_code == 200
data = resp.get_json()
assert data["search_history"] == []
def test_add_history_no_user_returns_401(self, authed_client):
"""When current_user returns None, add returns 401."""
cp = _common_patches()
with (
cp["db_manager"] as mdb,
patch(
"local_deep_research.web.auth.decorators.current_user",
return_value=None,
),
):
mdb.is_user_connected.return_value = True
resp = authed_client.post(
"/news/api/search-history",
json={"query": "test"},
)
assert resp.status_code == 401
def test_clear_history_no_user_returns_success(self, authed_client):
"""When current_user returns None, clear returns success."""
cp = _common_patches()
with (
cp["db_manager"] as mdb,
patch(
"local_deep_research.web.auth.decorators.current_user",
return_value=None,
),
):
mdb.is_user_connected.return_value = True
resp = authed_client.delete("/news/api/search-history")
assert resp.status_code == 200
data = resp.get_json()
assert data["status"] == "success"
# ---------------------------------------------------------------------------
# GET /scheduler/stats – last_activity None branch
# ---------------------------------------------------------------------------
class TestSchedulerStatsLastActivityNone:
"""Covers scheduler_stats when last_activity is None."""
def test_last_activity_none_serialized(self, authed_client):
"""When last_activity is None, it serializes as null."""
cp = _common_patches()
mock_scheduler = MagicMock()
mock_scheduler.is_running = True
mock_scheduler.user_sessions = {
"testuser": {
"last_activity": None,
"scheduled_jobs": set(),
}
}
mock_scheduler._credential_store.retrieve.return_value = None
mock_scheduler.scheduler = None # no APScheduler
with (
cp["db_manager"] as mdb,
patch(
"local_deep_research.scheduler.background.get_background_job_scheduler",
return_value=mock_scheduler,
),
patch(f"{MODULE}.get_env_setting", return_value=True),
):
mdb.is_user_connected.return_value = True
resp = authed_client.get("/news/api/scheduler/stats")
assert resp.status_code == 200
data = resp.get_json()
user_info = data["user_sessions"]["testuser"]
assert user_info["last_activity"] is None
assert user_info["has_password"] is False
# ---------------------------------------------------------------------------
# DELETE /subscriptions/<id> – success and not-found
# ---------------------------------------------------------------------------
class TestDeleteSubscriptionBranches:
"""Covers delete_subscription success and not-found paths."""
def test_successful_delete(self, authed_client):
"""Successful deletion returns success message."""
cp = _common_patches()
with (
cp["db_manager"] as mdb,
cp["get_user_id"],
patch(f"{MODULE}.api.delete_subscription", return_value=True),
):
mdb.is_user_connected.return_value = True
resp = authed_client.delete("/news/api/subscriptions/sub-1")
assert resp.status_code == 200
data = resp.get_json()
assert data["status"] == "success"
assert "sub-1" in data["message"]
def test_not_found_returns_404(self, authed_client):
"""When delete returns False, 404 is returned."""
cp = _common_patches()
with (
cp["db_manager"] as mdb,
cp["get_user_id"],
patch(f"{MODULE}.api.delete_subscription", return_value=False),
):
mdb.is_user_connected.return_value = True
resp = authed_client.delete("/news/api/subscriptions/sub-999")
assert resp.status_code == 404
# ---------------------------------------------------------------------------
# GET /subscriptions/current – error in result
# ---------------------------------------------------------------------------
class TestGetCurrentUserSubscriptionsErrorInResult:
"""Covers the error-in-result branch for get_current_user_subscriptions."""
def test_api_error_returns_500(self, authed_client):
"""When api.get_subscriptions returns error dict, returns 500."""
cp = _common_patches()
with (
cp["db_manager"] as mdb,
cp["get_user_id"],
patch(
f"{MODULE}.api.get_subscriptions",
return_value={"error": "DB connection lost"},
),
):
mdb.is_user_connected.return_value = True
resp = authed_client.get("/news/api/subscriptions/current")
assert resp.status_code == 500
assert (
"Failed to retrieve subscriptions" in resp.get_json()["error"]
)
# ---------------------------------------------------------------------------
# POST /research/<card_id> – exception
# ---------------------------------------------------------------------------
class TestResearchNewsItemException:
"""Covers exception path in research_news_item."""
def test_exception_returns_500(self, authed_client):
"""Exception during research returns 500 with safe message."""
cp = _common_patches()
with (
cp["db_manager"] as mdb,
cp["get_user_id"],
patch(
f"{MODULE}.api.research_news_item",
side_effect=RuntimeError("LLM unavailable"),
),
):
mdb.is_user_connected.return_value = True
resp = authed_client.post(
"/news/api/research/card-1",
json={"depth": "detailed"},
)
assert resp.status_code == 500
data = resp.get_json()
assert "error" in data
# Internal message should not leak
assert "LLM unavailable" not in data["error"]
# ---------------------------------------------------------------------------
# GET /subscription/stats – exception
# ---------------------------------------------------------------------------
class TestSubscriptionStatsException:
"""Covers exception path in get_subscription_stats."""
def test_exception_returns_500(self, authed_client):
"""Exception during stats retrieval returns 500."""
cp = _common_patches()
with (
cp["db_manager"] as mdb,
cp["get_user_id"],
patch(
f"{MODULE}.get_user_db_session",
side_effect=RuntimeError("DB locked"),
),
):
mdb.is_user_connected.return_value = True
resp = authed_client.get("/news/api/subscription/stats")
assert resp.status_code == 500
assert "error" in resp.get_json()