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
1979 lines
69 KiB
Python
1979 lines
69 KiB
Python
"""
|
|
Comprehensive tests for local_deep_research.news.flask_api endpoints.
|
|
|
|
These tests properly mock auth decorators and the underlying api module
|
|
functions to exercise actual endpoint logic, not just status codes.
|
|
"""
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
from flask import Flask
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _auth_session(client, username="testuser"):
|
|
"""Set up a valid authenticated session."""
|
|
with client.session_transaction() as sess:
|
|
sess["username"] = username
|
|
|
|
|
|
def _auth_patches():
|
|
"""Return a dict of common patches needed for auth + db plumbing."""
|
|
return {
|
|
"db_manager": patch(
|
|
"local_deep_research.web.auth.decorators.db_manager"
|
|
),
|
|
"get_user_id": patch(
|
|
"local_deep_research.news.flask_api.get_user_id",
|
|
return_value="testuser",
|
|
),
|
|
"get_settings_manager": patch(
|
|
"local_deep_research.news.flask_api.get_settings_manager",
|
|
),
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Fixtures
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.fixture
|
|
def app():
|
|
app = Flask(__name__)
|
|
app.config["SECRET_KEY"] = "test-secret-key"
|
|
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
|
|
|
|
|
|
@pytest.fixture
|
|
def client(app):
|
|
return app.test_client()
|
|
|
|
|
|
@pytest.fixture
|
|
def authed_client(client):
|
|
"""Return client with auth session pre-configured."""
|
|
_auth_session(client)
|
|
return client
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 1. GET /feed (get_news_feed)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestGetNewsFeed:
|
|
"""Tests for the /feed endpoint."""
|
|
|
|
def test_returns_news_items(self, authed_client):
|
|
patches = _auth_patches()
|
|
mock_result = {
|
|
"news_items": [{"id": "1", "title": "Test News"}],
|
|
"total": 1,
|
|
}
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patches["get_user_id"],
|
|
patches["get_settings_manager"] as mock_sm,
|
|
patch(
|
|
"local_deep_research.news.flask_api.api.get_news_feed",
|
|
return_value=mock_result,
|
|
) as mock_feed,
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
mock_sm_instance = MagicMock()
|
|
mock_sm_instance.get_setting.return_value = 20
|
|
mock_sm.return_value = mock_sm_instance
|
|
|
|
resp = authed_client.get("/news/api/feed")
|
|
assert resp.status_code == 200
|
|
data = resp.get_json()
|
|
assert len(data["news_items"]) == 1
|
|
assert data["news_items"][0]["title"] == "Test News"
|
|
|
|
mock_feed.assert_called_once_with(
|
|
user_id="testuser",
|
|
limit=20,
|
|
use_cache=True,
|
|
focus=None,
|
|
search_strategy=None,
|
|
subscription_id=None,
|
|
)
|
|
|
|
def test_with_query_params(self, authed_client):
|
|
patches = _auth_patches()
|
|
mock_result = {"news_items": [], "total": 0}
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patches["get_user_id"],
|
|
patches["get_settings_manager"] as mock_sm,
|
|
patch(
|
|
"local_deep_research.news.flask_api.api.get_news_feed",
|
|
return_value=mock_result,
|
|
) as mock_feed,
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
mock_sm_instance = MagicMock()
|
|
mock_sm_instance.get_setting.return_value = 20
|
|
mock_sm.return_value = mock_sm_instance
|
|
|
|
resp = authed_client.get(
|
|
"/news/api/feed?limit=5&strategy=latest&subscription_id=sub1&focus=tech"
|
|
)
|
|
assert resp.status_code == 200
|
|
|
|
mock_feed.assert_called_once_with(
|
|
user_id="testuser",
|
|
limit=5,
|
|
use_cache=True,
|
|
focus="tech",
|
|
search_strategy="latest",
|
|
subscription_id="sub1",
|
|
)
|
|
|
|
def test_use_cache_false(self, authed_client):
|
|
patches = _auth_patches()
|
|
mock_result = {"news_items": []}
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patches["get_user_id"],
|
|
patches["get_settings_manager"] as mock_sm,
|
|
patch(
|
|
"local_deep_research.news.flask_api.api.get_news_feed",
|
|
return_value=mock_result,
|
|
) as mock_feed,
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
mock_sm_instance = MagicMock()
|
|
mock_sm_instance.get_setting.return_value = 20
|
|
mock_sm.return_value = mock_sm_instance
|
|
|
|
resp = authed_client.get("/news/api/feed?use_cache=false")
|
|
assert resp.status_code == 200
|
|
mock_feed.assert_called_once()
|
|
assert mock_feed.call_args.kwargs["use_cache"] is False
|
|
|
|
def test_api_error_in_result(self, authed_client):
|
|
patches = _auth_patches()
|
|
mock_result = {
|
|
"error": "limit must be between 1 and 100",
|
|
"news_items": [],
|
|
}
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patches["get_user_id"],
|
|
patches["get_settings_manager"] as mock_sm,
|
|
patch(
|
|
"local_deep_research.news.flask_api.api.get_news_feed",
|
|
return_value=mock_result,
|
|
),
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
mock_sm_instance = MagicMock()
|
|
mock_sm_instance.get_setting.return_value = 20
|
|
mock_sm.return_value = mock_sm_instance
|
|
|
|
resp = authed_client.get("/news/api/feed?limit=-1")
|
|
assert resp.status_code == 400
|
|
data = resp.get_json()
|
|
assert "error" in data
|
|
|
|
def test_exception_returns_500(self, authed_client):
|
|
patches = _auth_patches()
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patches["get_user_id"],
|
|
patches["get_settings_manager"] as mock_sm,
|
|
patch(
|
|
"local_deep_research.news.flask_api.api.get_news_feed",
|
|
side_effect=RuntimeError("boom"),
|
|
),
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
mock_sm_instance = MagicMock()
|
|
mock_sm_instance.get_setting.return_value = 20
|
|
mock_sm.return_value = mock_sm_instance
|
|
|
|
resp = authed_client.get("/news/api/feed")
|
|
assert resp.status_code == 500
|
|
data = resp.get_json()
|
|
assert "error" in data
|
|
assert data["news_items"] == []
|
|
|
|
def test_unauthenticated_is_blocked(self, client):
|
|
"""Without session username, login_required returns JSON 401 for
|
|
the /news/api/feed API endpoint."""
|
|
resp = client.get("/news/api/feed")
|
|
assert resp.status_code == 401
|
|
assert resp.get_json()["error"] == "Authentication required"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 2. POST /subscribe (create_subscription)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestCreateSubscription:
|
|
"""Tests for the /subscribe endpoint."""
|
|
|
|
def test_successful_creation(self, authed_client):
|
|
patches = _auth_patches()
|
|
mock_result = {"id": "sub-123", "status": "active", "query": "AI news"}
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patches["get_user_id"],
|
|
patch(
|
|
"local_deep_research.news.flask_api.api.create_subscription",
|
|
return_value=mock_result,
|
|
) as mock_create,
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.post(
|
|
"/news/api/subscribe",
|
|
json={"query": "AI news", "subscription_type": "search"},
|
|
)
|
|
assert resp.status_code == 200
|
|
data = resp.get_json()
|
|
assert data["id"] == "sub-123"
|
|
|
|
mock_create.assert_called_once()
|
|
call_kwargs = mock_create.call_args.kwargs
|
|
assert call_kwargs["query"] == "AI news"
|
|
assert call_kwargs["user_id"] == "testuser"
|
|
assert call_kwargs["subscription_type"] == "search"
|
|
|
|
def test_missing_query_returns_400(self, authed_client):
|
|
patches = _auth_patches()
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patches["get_user_id"],
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.post(
|
|
"/news/api/subscribe",
|
|
json={"subscription_type": "search"},
|
|
)
|
|
assert resp.status_code == 400
|
|
data = resp.get_json()
|
|
assert "query" in data["error"].lower()
|
|
|
|
def test_no_json_body_returns_400(self, authed_client):
|
|
patches = _auth_patches()
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patches["get_user_id"],
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.post(
|
|
"/news/api/subscribe",
|
|
data="not json",
|
|
content_type="text/plain",
|
|
)
|
|
assert resp.status_code == 400
|
|
|
|
def test_with_all_optional_fields(self, authed_client):
|
|
patches = _auth_patches()
|
|
mock_result = {"id": "sub-456", "status": "active"}
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patches["get_user_id"],
|
|
patch(
|
|
"local_deep_research.news.flask_api.api.create_subscription",
|
|
return_value=mock_result,
|
|
) as mock_create,
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.post(
|
|
"/news/api/subscribe",
|
|
json={
|
|
"query": "ML papers",
|
|
"subscription_type": "topic",
|
|
"refresh_minutes": 60,
|
|
"model_provider": "OPENAI",
|
|
"model": "gpt-4",
|
|
"search_strategy": "deep",
|
|
"custom_endpoint": "https://example.com",
|
|
"name": "ML Feed",
|
|
"folder_id": "fold-1",
|
|
"is_active": True,
|
|
"search_engine": "google",
|
|
"search_iterations": 3,
|
|
"questions_per_iteration": 5,
|
|
},
|
|
)
|
|
assert resp.status_code == 200
|
|
call_kwargs = mock_create.call_args.kwargs
|
|
assert call_kwargs["model_provider"] == "openai"
|
|
assert call_kwargs["name"] == "ML Feed"
|
|
assert call_kwargs["search_iterations"] == 3
|
|
|
|
def test_value_error_returns_400(self, authed_client):
|
|
patches = _auth_patches()
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patches["get_user_id"],
|
|
patch(
|
|
"local_deep_research.news.flask_api.api.create_subscription",
|
|
side_effect=ValueError("bad value"),
|
|
),
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.post(
|
|
"/news/api/subscribe",
|
|
json={"query": "test"},
|
|
)
|
|
assert resp.status_code == 400
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 3. POST /vote (vote_on_news)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestVoteOnNews:
|
|
"""Tests for the /vote endpoint."""
|
|
|
|
def test_upvote(self, authed_client):
|
|
patches = _auth_patches()
|
|
mock_result = {"status": "success", "vote": "up"}
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patches["get_user_id"],
|
|
patch(
|
|
"local_deep_research.news.flask_api.api.submit_feedback",
|
|
return_value=mock_result,
|
|
) as mock_vote,
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.post(
|
|
"/news/api/vote",
|
|
json={"card_id": "card-1", "vote": "up"},
|
|
)
|
|
assert resp.status_code == 200
|
|
mock_vote.assert_called_once_with(
|
|
card_id="card-1", user_id="testuser", vote="up"
|
|
)
|
|
|
|
def test_downvote(self, authed_client):
|
|
patches = _auth_patches()
|
|
mock_result = {"status": "success", "vote": "down"}
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patches["get_user_id"],
|
|
patch(
|
|
"local_deep_research.news.flask_api.api.submit_feedback",
|
|
return_value=mock_result,
|
|
) as mock_vote,
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.post(
|
|
"/news/api/vote",
|
|
json={"card_id": "card-2", "vote": "down"},
|
|
)
|
|
assert resp.status_code == 200
|
|
mock_vote.assert_called_once_with(
|
|
card_id="card-2", user_id="testuser", vote="down"
|
|
)
|
|
|
|
def test_missing_card_id(self, authed_client):
|
|
patches = _auth_patches()
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patches["get_user_id"],
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.post(
|
|
"/news/api/vote",
|
|
json={"vote": "up"},
|
|
)
|
|
assert resp.status_code == 400
|
|
assert "required" in resp.get_json()["error"].lower()
|
|
|
|
def test_missing_vote(self, authed_client):
|
|
patches = _auth_patches()
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patches["get_user_id"],
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.post(
|
|
"/news/api/vote",
|
|
json={"card_id": "card-1"},
|
|
)
|
|
assert resp.status_code == 400
|
|
|
|
def test_not_found_error(self, authed_client):
|
|
patches = _auth_patches()
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patches["get_user_id"],
|
|
patch(
|
|
"local_deep_research.news.flask_api.api.submit_feedback",
|
|
side_effect=ValueError("Card not found"),
|
|
),
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.post(
|
|
"/news/api/vote",
|
|
json={"card_id": "bad-id", "vote": "up"},
|
|
)
|
|
assert resp.status_code == 404
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 4. POST /feedback/batch (get_batch_feedback)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestGetBatchFeedback:
|
|
"""Tests for the /feedback/batch endpoint."""
|
|
|
|
def test_get_votes_for_cards(self, authed_client):
|
|
patches = _auth_patches()
|
|
mock_result = {"votes": {"c1": "up", "c2": "down"}}
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patches["get_user_id"],
|
|
patch(
|
|
"local_deep_research.news.flask_api.api.get_votes_for_cards",
|
|
return_value=mock_result,
|
|
) as mock_batch,
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.post(
|
|
"/news/api/feedback/batch",
|
|
json={"card_ids": ["c1", "c2"]},
|
|
)
|
|
assert resp.status_code == 200
|
|
data = resp.get_json()
|
|
assert data["votes"]["c1"] == "up"
|
|
|
|
mock_batch.assert_called_once_with(
|
|
card_ids=["c1", "c2"], user_id="testuser"
|
|
)
|
|
|
|
def test_empty_card_ids_returns_empty(self, authed_client):
|
|
patches = _auth_patches()
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patches["get_user_id"],
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.post(
|
|
"/news/api/feedback/batch",
|
|
json={"card_ids": []},
|
|
)
|
|
assert resp.status_code == 200
|
|
assert resp.get_json()["votes"] == {}
|
|
|
|
def test_missing_card_ids_returns_empty(self, authed_client):
|
|
patches = _auth_patches()
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patches["get_user_id"],
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.post(
|
|
"/news/api/feedback/batch",
|
|
json={},
|
|
)
|
|
assert resp.status_code == 200
|
|
assert resp.get_json()["votes"] == {}
|
|
|
|
def test_value_error_not_found(self, authed_client):
|
|
patches = _auth_patches()
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patches["get_user_id"],
|
|
patch(
|
|
"local_deep_research.news.flask_api.api.get_votes_for_cards",
|
|
side_effect=ValueError("User not found"),
|
|
),
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.post(
|
|
"/news/api/feedback/batch",
|
|
json={"card_ids": ["c1"]},
|
|
)
|
|
assert resp.status_code == 404
|
|
|
|
def test_exception_returns_500(self, authed_client):
|
|
patches = _auth_patches()
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patches["get_user_id"],
|
|
patch(
|
|
"local_deep_research.news.flask_api.api.get_votes_for_cards",
|
|
side_effect=RuntimeError("db error"),
|
|
),
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.post(
|
|
"/news/api/feedback/batch",
|
|
json={"card_ids": ["c1"]},
|
|
)
|
|
assert resp.status_code == 500
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 5. POST /feedback/<card_id> (submit_feedback)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestSubmitFeedback:
|
|
"""Tests for the /feedback/<card_id> endpoint."""
|
|
|
|
def test_submit_vote(self, authed_client):
|
|
patches = _auth_patches()
|
|
mock_result = {"status": "recorded"}
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patches["get_user_id"],
|
|
patch(
|
|
"local_deep_research.news.flask_api.api.submit_feedback",
|
|
return_value=mock_result,
|
|
) as mock_fb,
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.post(
|
|
"/news/api/feedback/card-99",
|
|
json={"vote": "up"},
|
|
)
|
|
assert resp.status_code == 200
|
|
mock_fb.assert_called_once_with(
|
|
card_id="card-99", user_id="testuser", vote="up"
|
|
)
|
|
|
|
def test_missing_vote_returns_400(self, authed_client):
|
|
patches = _auth_patches()
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patches["get_user_id"],
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.post(
|
|
"/news/api/feedback/card-1",
|
|
json={"something": "else"},
|
|
)
|
|
assert resp.status_code == 400
|
|
assert "vote" in resp.get_json()["error"].lower()
|
|
|
|
def test_not_found_error(self, authed_client):
|
|
patches = _auth_patches()
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patches["get_user_id"],
|
|
patch(
|
|
"local_deep_research.news.flask_api.api.submit_feedback",
|
|
side_effect=ValueError("Card not found"),
|
|
),
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.post(
|
|
"/news/api/feedback/card-bad",
|
|
json={"vote": "up"},
|
|
)
|
|
assert resp.status_code == 404
|
|
|
|
def test_invalid_value_error(self, authed_client):
|
|
patches = _auth_patches()
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patches["get_user_id"],
|
|
patch(
|
|
"local_deep_research.news.flask_api.api.submit_feedback",
|
|
side_effect=ValueError("Vote must be up or down"),
|
|
),
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.post(
|
|
"/news/api/feedback/card-1",
|
|
json={"vote": "invalid"},
|
|
)
|
|
assert resp.status_code == 400
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 6. GET /subscriptions/current (get_current_user_subscriptions)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestGetCurrentUserSubscriptions:
|
|
"""Tests for the /subscriptions/current endpoint."""
|
|
|
|
def test_returns_subscriptions(self, authed_client):
|
|
patches = _auth_patches()
|
|
mock_result = {
|
|
"subscriptions": [
|
|
{"id": "s1", "query": "AI"},
|
|
{"id": "s2", "query": "ML"},
|
|
]
|
|
}
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patches["get_user_id"],
|
|
patch(
|
|
"local_deep_research.news.flask_api.api.get_subscriptions",
|
|
return_value=mock_result,
|
|
) as mock_subs,
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.get("/news/api/subscriptions/current")
|
|
assert resp.status_code == 200
|
|
data = resp.get_json()
|
|
assert len(data["subscriptions"]) == 2
|
|
mock_subs.assert_called_once_with("testuser")
|
|
|
|
def test_api_error_returns_500(self, authed_client):
|
|
patches = _auth_patches()
|
|
mock_result = {"error": "Database unavailable"}
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patches["get_user_id"],
|
|
patch(
|
|
"local_deep_research.news.flask_api.api.get_subscriptions",
|
|
return_value=mock_result,
|
|
),
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.get("/news/api/subscriptions/current")
|
|
assert resp.status_code == 500
|
|
|
|
def test_exception_returns_500(self, authed_client):
|
|
patches = _auth_patches()
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patches["get_user_id"],
|
|
patch(
|
|
"local_deep_research.news.flask_api.api.get_subscriptions",
|
|
side_effect=RuntimeError("crash"),
|
|
),
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.get("/news/api/subscriptions/current")
|
|
assert resp.status_code == 500
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 7. GET /subscriptions/<id> (get_subscription)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestGetSubscription:
|
|
"""Tests for the /subscriptions/<id> endpoint."""
|
|
|
|
def test_returns_subscription(self, authed_client):
|
|
patches = _auth_patches()
|
|
mock_result = {"id": "s1", "query": "AI news", "status": "active"}
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patch(
|
|
"local_deep_research.news.flask_api.api.get_subscription",
|
|
return_value=mock_result,
|
|
) as mock_get,
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.get("/news/api/subscriptions/s1")
|
|
assert resp.status_code == 200
|
|
data = resp.get_json()
|
|
assert data["id"] == "s1"
|
|
mock_get.assert_called_once_with("s1")
|
|
|
|
def test_not_found(self, authed_client):
|
|
patches = _auth_patches()
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patch(
|
|
"local_deep_research.news.flask_api.api.get_subscription",
|
|
return_value=None,
|
|
),
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.get("/news/api/subscriptions/nonexistent")
|
|
assert resp.status_code == 404
|
|
|
|
def test_null_id_returns_400(self, authed_client):
|
|
patches = _auth_patches()
|
|
with patches["db_manager"] as mock_db:
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.get("/news/api/subscriptions/null")
|
|
assert resp.status_code == 400
|
|
|
|
def test_undefined_id_returns_400(self, authed_client):
|
|
patches = _auth_patches()
|
|
with patches["db_manager"] as mock_db:
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.get("/news/api/subscriptions/undefined")
|
|
assert resp.status_code == 400
|
|
|
|
def test_exception_returns_500(self, authed_client):
|
|
patches = _auth_patches()
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patch(
|
|
"local_deep_research.news.flask_api.api.get_subscription",
|
|
side_effect=RuntimeError("db error"),
|
|
),
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.get("/news/api/subscriptions/s1")
|
|
assert resp.status_code == 500
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 8. PUT /subscriptions/<id> (update_subscription)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestUpdateSubscription:
|
|
"""Tests for the PUT /subscriptions/<id> endpoint."""
|
|
|
|
def test_successful_update(self, authed_client):
|
|
patches = _auth_patches()
|
|
mock_result = {"id": "s1", "status": "updated"}
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patch(
|
|
"local_deep_research.news.flask_api.api.update_subscription",
|
|
return_value=mock_result,
|
|
) as mock_update,
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.put(
|
|
"/news/api/subscriptions/s1",
|
|
json={"query": "Updated query", "name": "New Name"},
|
|
)
|
|
assert resp.status_code == 200
|
|
data = resp.get_json()
|
|
assert data["status"] == "updated"
|
|
|
|
# Verify field mapping: query -> query_or_topic
|
|
call_args = mock_update.call_args
|
|
assert call_args[0][0] == "s1"
|
|
update_data = call_args[0][1]
|
|
assert update_data["query_or_topic"] == "Updated query"
|
|
assert update_data["name"] == "New Name"
|
|
|
|
def test_field_mapping(self, authed_client):
|
|
"""Verify all field mappings work correctly."""
|
|
patches = _auth_patches()
|
|
mock_result = {"id": "s1", "status": "updated"}
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patch(
|
|
"local_deep_research.news.flask_api.api.update_subscription",
|
|
return_value=mock_result,
|
|
) as mock_update,
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.put(
|
|
"/news/api/subscriptions/s1",
|
|
json={
|
|
"refresh_minutes": 30,
|
|
"is_active": False,
|
|
"search_engine": "bing",
|
|
},
|
|
)
|
|
assert resp.status_code == 200
|
|
update_data = mock_update.call_args[0][1]
|
|
assert update_data["refresh_interval_minutes"] == 30
|
|
assert update_data["is_active"] is False
|
|
assert update_data["search_engine"] == "bing"
|
|
|
|
def test_error_in_result_not_found(self, authed_client):
|
|
patches = _auth_patches()
|
|
mock_result = {"error": "Subscription not found"}
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patch(
|
|
"local_deep_research.news.flask_api.api.update_subscription",
|
|
return_value=mock_result,
|
|
),
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.put(
|
|
"/news/api/subscriptions/bad-id",
|
|
json={"name": "test"},
|
|
)
|
|
assert resp.status_code == 404
|
|
|
|
def test_error_in_result_generic(self, authed_client):
|
|
patches = _auth_patches()
|
|
mock_result = {"error": "Invalid data provided"}
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patch(
|
|
"local_deep_research.news.flask_api.api.update_subscription",
|
|
return_value=mock_result,
|
|
),
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.put(
|
|
"/news/api/subscriptions/s1",
|
|
json={"name": "x"},
|
|
)
|
|
assert resp.status_code == 400
|
|
|
|
def test_no_json_body_returns_400(self, authed_client):
|
|
patches = _auth_patches()
|
|
with patches["db_manager"] as mock_db:
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.put(
|
|
"/news/api/subscriptions/s1",
|
|
data="not json",
|
|
content_type="text/plain",
|
|
)
|
|
assert resp.status_code == 400
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 9. DELETE /subscriptions/<id> (delete_subscription)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestDeleteSubscription:
|
|
"""Tests for the DELETE /subscriptions/<id> endpoint."""
|
|
|
|
def test_successful_delete(self, authed_client):
|
|
patches = _auth_patches()
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patch(
|
|
"local_deep_research.news.flask_api.api.delete_subscription",
|
|
return_value=True,
|
|
) as mock_del,
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.delete("/news/api/subscriptions/s1")
|
|
assert resp.status_code == 200
|
|
data = resp.get_json()
|
|
assert data["status"] == "success"
|
|
assert "s1" in data["message"]
|
|
mock_del.assert_called_once_with("s1")
|
|
|
|
def test_not_found(self, authed_client):
|
|
patches = _auth_patches()
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patch(
|
|
"local_deep_research.news.flask_api.api.delete_subscription",
|
|
return_value=False,
|
|
),
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.delete("/news/api/subscriptions/bad-id")
|
|
assert resp.status_code == 404
|
|
|
|
def test_exception_returns_500(self, authed_client):
|
|
patches = _auth_patches()
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patch(
|
|
"local_deep_research.news.flask_api.api.delete_subscription",
|
|
side_effect=RuntimeError("db error"),
|
|
),
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.delete("/news/api/subscriptions/s1")
|
|
assert resp.status_code == 500
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 10. GET /subscriptions/<id>/history (get_subscription_history)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestGetSubscriptionHistory:
|
|
"""Tests for the /subscriptions/<id>/history endpoint."""
|
|
|
|
def test_returns_history(self, authed_client):
|
|
patches = _auth_patches()
|
|
mock_result = {
|
|
"history": [
|
|
{"id": "h1", "date": "2026-01-01"},
|
|
{"id": "h2", "date": "2026-01-02"},
|
|
]
|
|
}
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patches["get_settings_manager"] as mock_sm,
|
|
patch(
|
|
"local_deep_research.news.flask_api.api.get_subscription_history",
|
|
return_value=mock_result,
|
|
) as mock_hist,
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
mock_sm_instance = MagicMock()
|
|
mock_sm_instance.get_setting.return_value = 20
|
|
mock_sm.return_value = mock_sm_instance
|
|
|
|
resp = authed_client.get("/news/api/subscriptions/s1/history")
|
|
assert resp.status_code == 200
|
|
data = resp.get_json()
|
|
assert len(data["history"]) == 2
|
|
mock_hist.assert_called_once_with("s1", 20)
|
|
|
|
def test_custom_limit(self, authed_client):
|
|
patches = _auth_patches()
|
|
mock_result = {"history": []}
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patches["get_settings_manager"] as mock_sm,
|
|
patch(
|
|
"local_deep_research.news.flask_api.api.get_subscription_history",
|
|
return_value=mock_result,
|
|
) as mock_hist,
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
mock_sm_instance = MagicMock()
|
|
mock_sm_instance.get_setting.return_value = 20
|
|
mock_sm.return_value = mock_sm_instance
|
|
|
|
resp = authed_client.get(
|
|
"/news/api/subscriptions/s1/history?limit=5"
|
|
)
|
|
assert resp.status_code == 200
|
|
mock_hist.assert_called_once_with("s1", 5)
|
|
|
|
def test_error_in_result(self, authed_client):
|
|
patches = _auth_patches()
|
|
mock_result = {"error": "Database error", "history": []}
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patches["get_settings_manager"] as mock_sm,
|
|
patch(
|
|
"local_deep_research.news.flask_api.api.get_subscription_history",
|
|
return_value=mock_result,
|
|
),
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
mock_sm_instance = MagicMock()
|
|
mock_sm_instance.get_setting.return_value = 20
|
|
mock_sm.return_value = mock_sm_instance
|
|
|
|
resp = authed_client.get("/news/api/subscriptions/s1/history")
|
|
assert resp.status_code == 500
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 11. POST /preferences (save_preferences)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestSavePreferences:
|
|
"""Tests for the /preferences endpoint."""
|
|
|
|
def test_save_preferences(self, authed_client):
|
|
patches = _auth_patches()
|
|
mock_result = {"status": "saved"}
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patches["get_user_id"],
|
|
patch(
|
|
"local_deep_research.news.flask_api.api.save_news_preferences",
|
|
return_value=mock_result,
|
|
) as mock_save,
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
prefs = {"theme": "dark", "language": "en"}
|
|
resp = authed_client.post(
|
|
"/news/api/preferences",
|
|
json={"preferences": prefs},
|
|
)
|
|
assert resp.status_code == 200
|
|
mock_save.assert_called_once_with("testuser", prefs)
|
|
|
|
def test_empty_preferences(self, authed_client):
|
|
patches = _auth_patches()
|
|
mock_result = {"status": "saved"}
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patches["get_user_id"],
|
|
patch(
|
|
"local_deep_research.news.flask_api.api.save_news_preferences",
|
|
return_value=mock_result,
|
|
) as mock_save,
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.post(
|
|
"/news/api/preferences",
|
|
json={},
|
|
)
|
|
assert resp.status_code == 200
|
|
mock_save.assert_called_once_with("testuser", {})
|
|
|
|
def test_exception_returns_500(self, authed_client):
|
|
patches = _auth_patches()
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patches["get_user_id"],
|
|
patch(
|
|
"local_deep_research.news.flask_api.api.save_news_preferences",
|
|
side_effect=RuntimeError("fail"),
|
|
),
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.post(
|
|
"/news/api/preferences",
|
|
json={"preferences": {"a": 1}},
|
|
)
|
|
assert resp.status_code == 500
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 12. GET /categories (get_categories)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestGetCategories:
|
|
"""Tests for the /categories endpoint."""
|
|
|
|
def test_returns_categories(self, authed_client):
|
|
mock_result = {
|
|
"categories": [
|
|
{"name": "tech", "count": 10},
|
|
{"name": "science", "count": 5},
|
|
]
|
|
}
|
|
patches = _auth_patches()
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patch(
|
|
"local_deep_research.news.flask_api.api.get_news_categories",
|
|
return_value=mock_result,
|
|
) as mock_cats,
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
resp = authed_client.get("/news/api/categories")
|
|
assert resp.status_code == 200
|
|
data = resp.get_json()
|
|
assert len(data["categories"]) == 2
|
|
mock_cats.assert_called_once()
|
|
|
|
def test_exception_returns_500(self, authed_client):
|
|
patches = _auth_patches()
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patch(
|
|
"local_deep_research.news.flask_api.api.get_news_categories",
|
|
side_effect=RuntimeError("fail"),
|
|
),
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
resp = authed_client.get("/news/api/categories")
|
|
assert resp.status_code == 500
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 13. GET /subscription/folders (get_folders)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestGetFolders:
|
|
"""Tests for the /subscription/folders GET endpoint."""
|
|
|
|
def test_returns_folders(self, authed_client):
|
|
patches = _auth_patches()
|
|
mock_folder = MagicMock()
|
|
mock_folder.to_dict.return_value = {"id": "f1", "name": "Work"}
|
|
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patches["get_user_id"],
|
|
patch(
|
|
"local_deep_research.news.flask_api.get_user_db_session"
|
|
) as mock_sess,
|
|
patch("local_deep_research.news.flask_api.FolderManager") as MockFM,
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
mock_sess.return_value.__enter__ = MagicMock(
|
|
return_value=MagicMock()
|
|
)
|
|
mock_sess.return_value.__exit__ = MagicMock(return_value=False)
|
|
MockFM.return_value.get_user_folders.return_value = [mock_folder]
|
|
|
|
resp = authed_client.get("/news/api/subscription/folders")
|
|
assert resp.status_code == 200
|
|
data = resp.get_json()
|
|
assert len(data) == 1
|
|
assert data[0]["name"] == "Work"
|
|
|
|
def test_exception_returns_500(self, authed_client):
|
|
patches = _auth_patches()
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patches["get_user_id"],
|
|
patch(
|
|
"local_deep_research.news.flask_api.get_user_db_session",
|
|
side_effect=RuntimeError("db fail"),
|
|
),
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.get("/news/api/subscription/folders")
|
|
assert resp.status_code == 500
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 14. POST /subscription/folders (create_folder)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestCreateFolder:
|
|
"""Tests for the POST /subscription/folders endpoint."""
|
|
|
|
def test_create_folder(self, authed_client):
|
|
patches = _auth_patches()
|
|
mock_folder = MagicMock()
|
|
mock_folder.to_dict.return_value = {
|
|
"id": "f-new",
|
|
"name": "Research",
|
|
}
|
|
mock_session = MagicMock()
|
|
mock_query = MagicMock()
|
|
mock_query.filter_by.return_value.first.return_value = None
|
|
mock_session.query.return_value = mock_query
|
|
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patch(
|
|
"local_deep_research.news.flask_api.get_user_db_session"
|
|
) as mock_sess,
|
|
patch("local_deep_research.news.flask_api.FolderManager") as MockFM,
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
mock_sess.return_value.__enter__ = MagicMock(
|
|
return_value=mock_session
|
|
)
|
|
mock_sess.return_value.__exit__ = MagicMock(return_value=False)
|
|
MockFM.return_value.create_folder.return_value = mock_folder
|
|
|
|
resp = authed_client.post(
|
|
"/news/api/subscription/folders",
|
|
json={"name": "Research", "description": "Research topics"},
|
|
)
|
|
assert resp.status_code == 201
|
|
data = resp.get_json()
|
|
assert data["name"] == "Research"
|
|
|
|
def test_missing_name_returns_400(self, authed_client):
|
|
patches = _auth_patches()
|
|
with patches["db_manager"] as mock_db:
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.post(
|
|
"/news/api/subscription/folders",
|
|
json={"description": "no name"},
|
|
)
|
|
assert resp.status_code == 400
|
|
|
|
def test_duplicate_folder_returns_409(self, authed_client):
|
|
patches = _auth_patches()
|
|
mock_session = MagicMock()
|
|
mock_existing = MagicMock()
|
|
mock_query = MagicMock()
|
|
mock_query.filter_by.return_value.first.return_value = mock_existing
|
|
mock_session.query.return_value = mock_query
|
|
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patch(
|
|
"local_deep_research.news.flask_api.get_user_db_session"
|
|
) as mock_sess,
|
|
patch("local_deep_research.news.flask_api.FolderManager"),
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
mock_sess.return_value.__enter__ = MagicMock(
|
|
return_value=mock_session
|
|
)
|
|
mock_sess.return_value.__exit__ = MagicMock(return_value=False)
|
|
|
|
resp = authed_client.post(
|
|
"/news/api/subscription/folders",
|
|
json={"name": "Existing"},
|
|
)
|
|
assert resp.status_code == 409
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 15. PUT /subscription/folders/<id> (update_folder)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestUpdateFolder:
|
|
"""Tests for the PUT /subscription/folders/<id> endpoint."""
|
|
|
|
def test_update_folder(self, authed_client):
|
|
patches = _auth_patches()
|
|
mock_folder = MagicMock()
|
|
mock_folder.to_dict.return_value = {"id": "f1", "name": "Updated"}
|
|
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patch(
|
|
"local_deep_research.news.flask_api.get_user_db_session"
|
|
) as mock_sess,
|
|
patch("local_deep_research.news.flask_api.FolderManager") as MockFM,
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
mock_sess.return_value.__enter__ = MagicMock(
|
|
return_value=MagicMock()
|
|
)
|
|
mock_sess.return_value.__exit__ = MagicMock(return_value=False)
|
|
MockFM.return_value.update_folder.return_value = mock_folder
|
|
|
|
resp = authed_client.put(
|
|
"/news/api/subscription/folders/f1",
|
|
json={"name": "Updated"},
|
|
)
|
|
assert resp.status_code == 200
|
|
data = resp.get_json()
|
|
assert data["name"] == "Updated"
|
|
|
|
def test_folder_not_found(self, authed_client):
|
|
patches = _auth_patches()
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patch(
|
|
"local_deep_research.news.flask_api.get_user_db_session"
|
|
) as mock_sess,
|
|
patch("local_deep_research.news.flask_api.FolderManager") as MockFM,
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
mock_sess.return_value.__enter__ = MagicMock(
|
|
return_value=MagicMock()
|
|
)
|
|
mock_sess.return_value.__exit__ = MagicMock(return_value=False)
|
|
MockFM.return_value.update_folder.return_value = None
|
|
|
|
resp = authed_client.put(
|
|
"/news/api/subscription/folders/bad-id",
|
|
json={"name": "X"},
|
|
)
|
|
assert resp.status_code == 404
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 16. DELETE /subscription/folders/<id> (delete_folder)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestDeleteFolder:
|
|
"""Tests for the DELETE /subscription/folders/<id> endpoint."""
|
|
|
|
def test_delete_folder(self, authed_client):
|
|
patches = _auth_patches()
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patch(
|
|
"local_deep_research.news.flask_api.get_user_db_session"
|
|
) as mock_sess,
|
|
patch("local_deep_research.news.flask_api.FolderManager") as MockFM,
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
mock_sess.return_value.__enter__ = MagicMock(
|
|
return_value=MagicMock()
|
|
)
|
|
mock_sess.return_value.__exit__ = MagicMock(return_value=False)
|
|
MockFM.return_value.delete_folder.return_value = True
|
|
|
|
resp = authed_client.delete("/news/api/subscription/folders/f1")
|
|
assert resp.status_code == 200
|
|
assert resp.get_json()["status"] == "deleted"
|
|
|
|
def test_delete_folder_not_found(self, authed_client):
|
|
patches = _auth_patches()
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patch(
|
|
"local_deep_research.news.flask_api.get_user_db_session"
|
|
) as mock_sess,
|
|
patch("local_deep_research.news.flask_api.FolderManager") as MockFM,
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
mock_sess.return_value.__enter__ = MagicMock(
|
|
return_value=MagicMock()
|
|
)
|
|
mock_sess.return_value.__exit__ = MagicMock(return_value=False)
|
|
MockFM.return_value.delete_folder.return_value = False
|
|
|
|
resp = authed_client.delete("/news/api/subscription/folders/bad-id")
|
|
assert resp.status_code == 404
|
|
|
|
def test_delete_folder_with_move_to(self, authed_client):
|
|
patches = _auth_patches()
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patch(
|
|
"local_deep_research.news.flask_api.get_user_db_session"
|
|
) as mock_sess,
|
|
patch("local_deep_research.news.flask_api.FolderManager") as MockFM,
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
mock_sess.return_value.__enter__ = MagicMock(
|
|
return_value=MagicMock()
|
|
)
|
|
mock_sess.return_value.__exit__ = MagicMock(return_value=False)
|
|
MockFM.return_value.delete_folder.return_value = True
|
|
|
|
resp = authed_client.delete(
|
|
"/news/api/subscription/folders/f1?move_to=f2"
|
|
)
|
|
assert resp.status_code == 200
|
|
MockFM.return_value.delete_folder.assert_called_once_with(
|
|
"f1", "f2"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 17. GET /subscription/subscriptions/organized (get_subscriptions_organized)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestGetSubscriptionsOrganized:
|
|
"""Tests for the /subscription/subscriptions/organized endpoint."""
|
|
|
|
def test_returns_organized(self, authed_client):
|
|
# get_subscriptions_by_folder returns plain dicts in a
|
|
# {"folders": [{"folder": {...}, "subscriptions": [...]}],
|
|
# "uncategorized": [...]} shape; the route flattens to
|
|
# {folder_name: [subscription, ...]}. (Calling .to_dict() on those
|
|
# dicts previously 500'd the route.)
|
|
patches = _auth_patches()
|
|
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patches["get_user_id"],
|
|
patch(
|
|
"local_deep_research.news.flask_api.get_user_db_session"
|
|
) as mock_sess,
|
|
patch("local_deep_research.news.flask_api.FolderManager") as MockFM,
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
mock_sess.return_value.__enter__ = MagicMock(
|
|
return_value=MagicMock()
|
|
)
|
|
mock_sess.return_value.__exit__ = MagicMock(return_value=False)
|
|
MockFM.return_value.get_subscriptions_by_folder.return_value = {
|
|
"folders": [
|
|
{
|
|
"folder": {"id": "f1", "name": "Work"},
|
|
"subscriptions": [{"id": "s1", "query_or_topic": "AI"}],
|
|
},
|
|
],
|
|
"uncategorized": [],
|
|
}
|
|
|
|
resp = authed_client.get(
|
|
"/news/api/subscription/subscriptions/organized"
|
|
)
|
|
assert resp.status_code == 200
|
|
data = resp.get_json()
|
|
assert data["Work"] == [{"id": "s1", "query_or_topic": "AI"}]
|
|
assert data["uncategorized"] == []
|
|
|
|
def test_exception_returns_500(self, authed_client):
|
|
patches = _auth_patches()
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patches["get_user_id"],
|
|
patch(
|
|
"local_deep_research.news.flask_api.get_user_db_session",
|
|
side_effect=RuntimeError("db fail"),
|
|
),
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.get(
|
|
"/news/api/subscription/subscriptions/organized"
|
|
)
|
|
assert resp.status_code == 500
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 18. GET /subscription/stats (get_subscription_stats)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestGetSubscriptionStats:
|
|
"""Tests for the /subscription/stats endpoint."""
|
|
|
|
def test_returns_stats(self, authed_client):
|
|
patches = _auth_patches()
|
|
mock_stats = {"total": 5, "active": 3, "paused": 2}
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patches["get_user_id"],
|
|
patch(
|
|
"local_deep_research.news.flask_api.get_user_db_session"
|
|
) as mock_sess,
|
|
patch("local_deep_research.news.flask_api.FolderManager") as MockFM,
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
mock_sess.return_value.__enter__ = MagicMock(
|
|
return_value=MagicMock()
|
|
)
|
|
mock_sess.return_value.__exit__ = MagicMock(return_value=False)
|
|
MockFM.return_value.get_subscription_stats.return_value = mock_stats
|
|
|
|
resp = authed_client.get("/news/api/subscription/stats")
|
|
assert resp.status_code == 200
|
|
data = resp.get_json()
|
|
assert data["total"] == 5
|
|
assert data["active"] == 3
|
|
|
|
def test_exception_returns_500(self, authed_client):
|
|
patches = _auth_patches()
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patches["get_user_id"],
|
|
patch(
|
|
"local_deep_research.news.flask_api.get_user_db_session",
|
|
side_effect=RuntimeError("db fail"),
|
|
),
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.get("/news/api/subscription/stats")
|
|
assert resp.status_code == 500
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 19. GET /search-history (get_search_history)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestGetSearchHistory:
|
|
"""Tests for the GET /search-history endpoint."""
|
|
|
|
def test_returns_history(self, authed_client):
|
|
patches = _auth_patches()
|
|
mock_item = MagicMock()
|
|
mock_item.to_dict.return_value = {
|
|
"id": 1,
|
|
"query": "test",
|
|
"type": "filter",
|
|
}
|
|
mock_db_session = MagicMock()
|
|
mock_db_session.query.return_value.order_by.return_value.limit.return_value.all.return_value = [
|
|
mock_item
|
|
]
|
|
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patch(
|
|
"local_deep_research.web.auth.decorators.current_user",
|
|
return_value="testuser",
|
|
),
|
|
patch(
|
|
"local_deep_research.database.session_context.get_user_db_session"
|
|
) as mock_sess,
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
mock_sess.return_value.__enter__ = MagicMock(
|
|
return_value=mock_db_session
|
|
)
|
|
mock_sess.return_value.__exit__ = MagicMock(return_value=False)
|
|
|
|
resp = authed_client.get("/news/api/search-history")
|
|
assert resp.status_code == 200
|
|
data = resp.get_json()
|
|
assert len(data["search_history"]) == 1
|
|
assert data["search_history"][0]["query"] == "test"
|
|
|
|
def test_unauthenticated_returns_empty(self, authed_client):
|
|
"""When current_user returns None, should return empty list."""
|
|
patches = _auth_patches()
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patch(
|
|
"local_deep_research.web.auth.decorators.current_user",
|
|
return_value=None,
|
|
),
|
|
):
|
|
mock_db.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_exception_returns_500(self, authed_client):
|
|
patches = _auth_patches()
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patch(
|
|
"local_deep_research.web.auth.decorators.current_user",
|
|
return_value="testuser",
|
|
),
|
|
patch(
|
|
"local_deep_research.database.session_context.get_user_db_session",
|
|
side_effect=RuntimeError("db error"),
|
|
),
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.get("/news/api/search-history")
|
|
assert resp.status_code == 500
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 20. POST /search-history (add_search_history)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestAddSearchHistory:
|
|
"""Tests for the POST /search-history endpoint."""
|
|
|
|
def test_add_search(self, authed_client):
|
|
patches = _auth_patches()
|
|
mock_db_session = MagicMock()
|
|
mock_history_instance = MagicMock()
|
|
mock_history_instance.id = 42
|
|
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patch(
|
|
"local_deep_research.web.auth.decorators.current_user",
|
|
return_value="testuser",
|
|
),
|
|
patch(
|
|
"local_deep_research.database.session_context.get_user_db_session"
|
|
) as mock_sess,
|
|
patch(
|
|
"local_deep_research.database.models.UserNewsSearchHistory",
|
|
return_value=mock_history_instance,
|
|
),
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
mock_sess.return_value.__enter__ = MagicMock(
|
|
return_value=mock_db_session
|
|
)
|
|
mock_sess.return_value.__exit__ = MagicMock(return_value=False)
|
|
|
|
resp = authed_client.post(
|
|
"/news/api/search-history",
|
|
json={
|
|
"query": "test search",
|
|
"type": "filter",
|
|
"resultCount": 10,
|
|
},
|
|
)
|
|
assert resp.status_code == 200
|
|
data = resp.get_json()
|
|
assert data["status"] == "success"
|
|
mock_db_session.add.assert_called_once()
|
|
mock_db_session.commit.assert_called_once()
|
|
|
|
def test_missing_query_returns_400(self, authed_client):
|
|
patches = _auth_patches()
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patch(
|
|
"local_deep_research.web.auth.decorators.current_user",
|
|
return_value="testuser",
|
|
),
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.post(
|
|
"/news/api/search-history",
|
|
json={"type": "filter"},
|
|
)
|
|
assert resp.status_code == 400
|
|
|
|
def test_no_body_returns_400(self, authed_client):
|
|
patches = _auth_patches()
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patch(
|
|
"local_deep_research.web.auth.decorators.current_user",
|
|
return_value="testuser",
|
|
),
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.post(
|
|
"/news/api/search-history",
|
|
json={},
|
|
)
|
|
assert resp.status_code == 400
|
|
|
|
def test_unauthenticated_returns_401(self, authed_client):
|
|
patches = _auth_patches()
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patch(
|
|
"local_deep_research.web.auth.decorators.current_user",
|
|
return_value=None,
|
|
),
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.post(
|
|
"/news/api/search-history",
|
|
json={"query": "test"},
|
|
)
|
|
assert resp.status_code == 401
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 21. DELETE /search-history (clear_search_history)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestClearSearchHistory:
|
|
"""Tests for the DELETE /search-history endpoint."""
|
|
|
|
def test_clear_history(self, authed_client):
|
|
patches = _auth_patches()
|
|
mock_db_session = MagicMock()
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patch(
|
|
"local_deep_research.web.auth.decorators.current_user",
|
|
return_value="testuser",
|
|
),
|
|
patch(
|
|
"local_deep_research.database.session_context.get_user_db_session"
|
|
) as mock_sess,
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
mock_sess.return_value.__enter__ = MagicMock(
|
|
return_value=mock_db_session
|
|
)
|
|
mock_sess.return_value.__exit__ = MagicMock(return_value=False)
|
|
|
|
resp = authed_client.delete("/news/api/search-history")
|
|
assert resp.status_code == 200
|
|
data = resp.get_json()
|
|
assert data["status"] == "success"
|
|
mock_db_session.query.return_value.delete.assert_called_once()
|
|
mock_db_session.commit.assert_called_once()
|
|
|
|
def test_unauthenticated_returns_success(self, authed_client):
|
|
"""When current_user returns None, endpoint returns success (no-op)."""
|
|
patches = _auth_patches()
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patch(
|
|
"local_deep_research.web.auth.decorators.current_user",
|
|
return_value=None,
|
|
),
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.delete("/news/api/search-history")
|
|
assert resp.status_code == 200
|
|
assert resp.get_json()["status"] == "success"
|
|
|
|
def test_exception_returns_500(self, authed_client):
|
|
patches = _auth_patches()
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patch(
|
|
"local_deep_research.web.auth.decorators.current_user",
|
|
return_value="testuser",
|
|
),
|
|
patch(
|
|
"local_deep_research.database.session_context.get_user_db_session",
|
|
side_effect=RuntimeError("db error"),
|
|
),
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.delete("/news/api/search-history")
|
|
assert resp.status_code == 500
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 22. POST /check-overdue (check_overdue_subscriptions)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestCheckOverdueSubscriptions:
|
|
"""Tests for the /check-overdue endpoint."""
|
|
|
|
def test_no_overdue_subscriptions(self, authed_client):
|
|
patches = _auth_patches()
|
|
mock_db_session = MagicMock()
|
|
mock_db_session.query.return_value.filter.return_value.all.return_value = []
|
|
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patch(
|
|
"local_deep_research.database.session_context.get_user_db_session"
|
|
) as mock_sess,
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
mock_sess.return_value.__enter__ = MagicMock(
|
|
return_value=mock_db_session
|
|
)
|
|
mock_sess.return_value.__exit__ = MagicMock(return_value=False)
|
|
|
|
resp = authed_client.post("/news/api/check-overdue")
|
|
assert resp.status_code == 200
|
|
data = resp.get_json()
|
|
assert data["status"] == "success"
|
|
assert data["overdue_found"] == 0
|
|
assert data["started"] == 0
|
|
|
|
def test_exception_returns_500(self, authed_client):
|
|
patches = _auth_patches()
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patch(
|
|
"local_deep_research.database.session_context.get_user_db_session",
|
|
side_effect=RuntimeError("db error"),
|
|
),
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.post("/news/api/check-overdue")
|
|
assert resp.status_code == 500
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Additional: Scheduler endpoints
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestSchedulerStatus:
|
|
"""Tests for the /scheduler/status endpoint."""
|
|
|
|
def test_returns_status(self, authed_client):
|
|
patches = _auth_patches()
|
|
mock_scheduler = MagicMock()
|
|
mock_scheduler.is_running = True
|
|
mock_scheduler.config = {"interval": 60}
|
|
mock_scheduler.user_sessions = {"testuser": {"scheduled_jobs": {"j1"}}}
|
|
mock_scheduler.scheduler = None
|
|
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patch(
|
|
"local_deep_research.scheduler.background.get_background_job_scheduler",
|
|
return_value=mock_scheduler,
|
|
),
|
|
patch(
|
|
"local_deep_research.news.flask_api.get_env_setting",
|
|
return_value=False,
|
|
),
|
|
):
|
|
mock_db.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["is_running"] is True
|
|
assert data["scheduler_available"] is True
|
|
|
|
def test_exception_returns_500(self, authed_client):
|
|
patches = _auth_patches()
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patch(
|
|
"local_deep_research.scheduler.background.get_background_job_scheduler",
|
|
side_effect=RuntimeError("fail"),
|
|
),
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.get("/news/api/scheduler/status")
|
|
assert resp.status_code == 500
|
|
|
|
|
|
class TestSchedulerStartStop:
|
|
"""Tests for scheduler start/stop endpoints."""
|
|
|
|
def test_start_scheduler(self, authed_client):
|
|
patches = _auth_patches()
|
|
mock_scheduler = MagicMock()
|
|
mock_scheduler.is_running = False
|
|
mock_scheduler.user_sessions = {}
|
|
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patch(
|
|
"local_deep_research.news.flask_api.get_env_setting",
|
|
return_value=True,
|
|
),
|
|
patch(
|
|
"local_deep_research.scheduler.background.get_background_job_scheduler",
|
|
return_value=mock_scheduler,
|
|
),
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.post("/news/api/scheduler/start")
|
|
assert resp.status_code == 200
|
|
data = resp.get_json()
|
|
assert data["status"] == "success"
|
|
mock_scheduler.start.assert_called_once()
|
|
|
|
def test_start_already_running(self, authed_client):
|
|
patches = _auth_patches()
|
|
mock_scheduler = MagicMock()
|
|
mock_scheduler.is_running = True
|
|
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patch(
|
|
"local_deep_research.news.flask_api.get_env_setting",
|
|
return_value=True,
|
|
),
|
|
patch(
|
|
"local_deep_research.scheduler.background.get_background_job_scheduler",
|
|
return_value=mock_scheduler,
|
|
),
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.post("/news/api/scheduler/start")
|
|
assert resp.status_code == 200
|
|
assert "already running" in resp.get_json()["message"].lower()
|
|
|
|
def test_start_blocked_by_setting(self, authed_client):
|
|
patches = _auth_patches()
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patch(
|
|
"local_deep_research.news.flask_api.get_env_setting",
|
|
return_value=False,
|
|
),
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.post("/news/api/scheduler/start")
|
|
assert resp.status_code == 403
|
|
|
|
def test_stop_scheduler(self, authed_client, app):
|
|
patches = _auth_patches()
|
|
mock_scheduler = MagicMock()
|
|
mock_scheduler.is_running = True
|
|
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patch(
|
|
"local_deep_research.news.flask_api.get_env_setting",
|
|
return_value=True,
|
|
),
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
app.background_job_scheduler = mock_scheduler
|
|
|
|
resp = authed_client.post("/news/api/scheduler/stop")
|
|
assert resp.status_code == 200
|
|
mock_scheduler.stop.assert_called_once()
|
|
|
|
def test_stop_no_scheduler(self, authed_client, app):
|
|
patches = _auth_patches()
|
|
with (
|
|
patches["db_manager"] as mock_db,
|
|
patch(
|
|
"local_deep_research.news.flask_api.get_env_setting",
|
|
return_value=True,
|
|
),
|
|
):
|
|
mock_db.is_user_connected.return_value = True
|
|
# Ensure no background_job_scheduler attribute
|
|
if hasattr(app, "background_job_scheduler"):
|
|
delattr(app, "background_job_scheduler")
|
|
|
|
resp = authed_client.post("/news/api/scheduler/stop")
|
|
assert resp.status_code == 404
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Edge case: require_json_body decorator
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestRequireJsonBody:
|
|
"""Test that endpoints with @require_json_body reject non-JSON requests."""
|
|
|
|
def test_vote_without_json(self, authed_client):
|
|
patches = _auth_patches()
|
|
with patches["db_manager"] as mock_db:
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.post(
|
|
"/news/api/vote",
|
|
data="plain text",
|
|
content_type="text/plain",
|
|
)
|
|
assert resp.status_code == 400
|
|
|
|
def test_feedback_batch_without_json(self, authed_client):
|
|
patches = _auth_patches()
|
|
with patches["db_manager"] as mock_db:
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.post(
|
|
"/news/api/feedback/batch",
|
|
data="plain text",
|
|
content_type="text/plain",
|
|
)
|
|
assert resp.status_code == 400
|
|
|
|
def test_preferences_without_json(self, authed_client):
|
|
patches = _auth_patches()
|
|
with patches["db_manager"] as mock_db:
|
|
mock_db.is_user_connected.return_value = True
|
|
|
|
resp = authed_client.post(
|
|
"/news/api/preferences",
|
|
data="plain text",
|
|
content_type="text/plain",
|
|
)
|
|
assert resp.status_code == 400
|