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
281 lines
10 KiB
Python
281 lines
10 KiB
Python
"""
|
|
Tests for news/flask_api.py.
|
|
|
|
Covers:
|
|
- safe_error_message helper (sanitises internal error details)
|
|
- get_user_id helper
|
|
- Error handlers (400 / 404 / 500)
|
|
- A parametrised auth-required smoke test for every news API endpoint
|
|
- One path-traversal guard test (Werkzeug rejects before route match)
|
|
- Two SUT integration tests that bypass auth via decorator patching
|
|
|
|
Original history: this file used to contain 83 tests, of which 71 were
|
|
near-identical "hit endpoint without auth, accept any of [200, 302, 401,
|
|
403, 404, 500]" tautologies. PR #4098 narrowed the assertions to
|
|
== 401 / == 404. This PR consolidates the 70 endpoint-+-method
|
|
auth-required tests into a single parametrised test
|
|
(`test_endpoint_requires_auth`) and keeps the path-traversal test
|
|
separately.
|
|
"""
|
|
|
|
import pytest
|
|
from unittest.mock import patch
|
|
from flask import Flask
|
|
|
|
|
|
@pytest.fixture
|
|
def app():
|
|
"""Create a Flask app for testing."""
|
|
app = Flask(__name__)
|
|
app.config["SECRET_KEY"] = "test-secret-key"
|
|
app.config["WTF_CSRF_ENABLED"] = False
|
|
app.config["TESTING"] = True
|
|
|
|
# Import and register the blueprints
|
|
from local_deep_research.news.flask_api import news_api_bp
|
|
from local_deep_research.web.auth import auth_bp
|
|
|
|
app.register_blueprint(auth_bp)
|
|
app.register_blueprint(news_api_bp, url_prefix="/news/api")
|
|
|
|
return app
|
|
|
|
|
|
@pytest.fixture
|
|
def client(app):
|
|
"""Create a test client."""
|
|
return app.test_client()
|
|
|
|
|
|
# ============= Auth-required endpoint smoke tests =============
|
|
|
|
# Every endpoint under /news/api/* is wrapped in @login_required. An
|
|
# unauthenticated request should be rejected with HTTP 401 by Flask-Login
|
|
# (the JSON-mode error handler kicks in because these routes return JSON).
|
|
#
|
|
# Before this consolidation each row below was its own ~5-line test with
|
|
# a misleading name like `test_feed_with_limit_parameter` — those tests
|
|
# were never actually exercising the parameter; they just bounced on 401
|
|
# regardless of what they sent. The parameters are kept here purely to
|
|
# preserve the URL surface that was probed previously.
|
|
AUTH_REQUIRED_ENDPOINTS = [
|
|
# GET endpoints
|
|
("GET", "/news/api/categories"),
|
|
("GET", "/news/api/feed?focus=technology"),
|
|
("GET", "/news/api/feed?limit=-1"),
|
|
("GET", "/news/api/feed?limit=0"),
|
|
("GET", "/news/api/feed?limit=10"),
|
|
("GET", "/news/api/feed?limit=999999"),
|
|
("GET", "/news/api/feed?limit=abc"),
|
|
("GET", "/news/api/feed?strategy=news_aggregation"),
|
|
("GET", "/news/api/feed?subscription_id=sub123"),
|
|
("GET", "/news/api/feed?use_cache=false"),
|
|
("GET", "/news/api/scheduler/stats"),
|
|
("GET", "/news/api/scheduler/status"),
|
|
("GET", "/news/api/scheduler/users"),
|
|
("GET", "/news/api/search-history"),
|
|
("GET", "/news/api/subscription/folders"),
|
|
("GET", "/news/api/subscription/stats"),
|
|
("GET", "/news/api/subscription/subscriptions/organized"),
|
|
("GET", "/news/api/subscriptions/'; DROP TABLE users; --"),
|
|
("GET", "/news/api/subscriptions/current"),
|
|
("GET", "/news/api/subscriptions/null"),
|
|
("GET", "/news/api/subscriptions/sub123"),
|
|
("GET", "/news/api/subscriptions/sub123/history"),
|
|
("GET", "/news/api/subscriptions/sub123/history?limit=50"),
|
|
("GET", "/news/api/subscriptions/undefined"),
|
|
# POST endpoints
|
|
("POST", "/news/api/check-overdue"),
|
|
("POST", "/news/api/feedback/batch"),
|
|
("POST", "/news/api/feedback/card123"),
|
|
("POST", "/news/api/preferences"),
|
|
("POST", "/news/api/research/card123"),
|
|
("POST", "/news/api/scheduler/check-now"),
|
|
("POST", "/news/api/scheduler/cleanup-now"),
|
|
("POST", "/news/api/scheduler/start"),
|
|
("POST", "/news/api/scheduler/stop"),
|
|
("POST", "/news/api/search-history"),
|
|
("POST", "/news/api/subscribe"),
|
|
("POST", "/news/api/subscription/folders"),
|
|
("POST", "/news/api/subscriptions/sub123/run"),
|
|
("POST", "/news/api/vote"),
|
|
# PUT endpoints
|
|
("PUT", "/news/api/subscription/folders/folder123"),
|
|
("PUT", "/news/api/subscription/subscriptions/sub123"),
|
|
("PUT", "/news/api/subscriptions/sub123"),
|
|
# DELETE endpoints
|
|
("DELETE", "/news/api/search-history"),
|
|
("DELETE", "/news/api/subscription/folders/folder123"),
|
|
("DELETE", "/news/api/subscription/folders/folder123?move_to=other_folder"),
|
|
("DELETE", "/news/api/subscriptions/nonexistent123"),
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize(("method", "url"), AUTH_REQUIRED_ENDPOINTS)
|
|
def test_endpoint_requires_auth(client, method, url):
|
|
"""Unauthenticated requests to /news/api/* must return 401.
|
|
|
|
This replaces ~70 individual tests that all asserted the same
|
|
contract via misleading test names like `test_feed_with_X_parameter`.
|
|
None of those tests actually exercised their parameters — they all
|
|
bounced on @login_required at 401. The single assertion here covers
|
|
the same contract more honestly and at a fraction of the cost.
|
|
"""
|
|
response = client.open(url, method=method)
|
|
assert response.status_code == 401, (
|
|
f"{method} {url} returned {response.status_code}, expected 401"
|
|
)
|
|
|
|
|
|
def test_path_traversal_rejected(client):
|
|
"""Path traversal in subscription id is rejected before route matching.
|
|
|
|
Werkzeug returns 404 (no matching route) for ../../etc/passwd-style
|
|
paths, so the request never reaches @login_required or the handler.
|
|
Either outcome (404 or 401) confirms the traversal is not honored;
|
|
we assert the specific code Werkzeug actually produces.
|
|
"""
|
|
response = client.get("/news/api/subscriptions/../../etc/passwd")
|
|
assert response.status_code == 404, response.status_code
|
|
|
|
|
|
# ============= safe_error_message tests =============
|
|
|
|
|
|
class TestSafeErrorMessageExtended:
|
|
"""Extended tests for safe_error_message function."""
|
|
|
|
def test_attribute_error(self):
|
|
"""Test handling of AttributeError."""
|
|
from local_deep_research.news.flask_api import safe_error_message
|
|
|
|
error = AttributeError("'NoneType' object has no attribute 'foo'")
|
|
result = safe_error_message(error, "processing data")
|
|
|
|
assert "An error occurred" in result
|
|
assert "processing data" in result
|
|
# Internal details should not be exposed
|
|
assert "NoneType" not in result
|
|
|
|
def test_io_error(self):
|
|
"""Test handling of IOError."""
|
|
from local_deep_research.news.flask_api import safe_error_message
|
|
|
|
error = IOError("Permission denied: /etc/passwd")
|
|
result = safe_error_message(error, "reading file")
|
|
|
|
assert "An error occurred" in result
|
|
# Path should not be exposed
|
|
assert "/etc/passwd" not in result
|
|
|
|
def test_index_error(self):
|
|
"""Test handling of IndexError."""
|
|
from local_deep_research.news.flask_api import safe_error_message
|
|
|
|
error = IndexError("list index out of range")
|
|
result = safe_error_message(error, "accessing list")
|
|
|
|
assert "An error occurred" in result
|
|
|
|
def test_connection_error(self):
|
|
"""Test handling of ConnectionError."""
|
|
from local_deep_research.news.flask_api import safe_error_message
|
|
|
|
error = ConnectionError("Connection refused to localhost:5000")
|
|
result = safe_error_message(error, "connecting to service")
|
|
|
|
assert "An error occurred" in result
|
|
# Internal service details should not be exposed
|
|
assert "localhost" not in result
|
|
|
|
def test_unicode_error_message(self):
|
|
"""Test handling of unicode characters in error message."""
|
|
from local_deep_research.news.flask_api import safe_error_message
|
|
|
|
error = ValueError("Invalid value: \u4e2d\u6587")
|
|
result = safe_error_message(error, "parsing")
|
|
|
|
# Should not crash on unicode
|
|
assert "Invalid input provided" in result
|
|
|
|
|
|
# ============= get_user_id Tests =============
|
|
|
|
|
|
# ============= get_user_id tests =============
|
|
|
|
|
|
class TestGetUserIdExtended:
|
|
"""Extended tests for get_user_id function."""
|
|
|
|
def test_get_user_id_empty_string(self, app):
|
|
"""Test getting user ID when username is empty string."""
|
|
from local_deep_research.news.flask_api import get_user_id
|
|
|
|
with app.app_context():
|
|
with patch(
|
|
"local_deep_research.web.auth.decorators.current_user",
|
|
return_value="",
|
|
):
|
|
result = get_user_id()
|
|
# Empty string is falsy, should return None
|
|
assert result is None
|
|
|
|
def test_get_user_id_special_characters(self, app):
|
|
"""Test getting user ID with special characters."""
|
|
from local_deep_research.news.flask_api import get_user_id
|
|
|
|
with app.app_context():
|
|
with patch(
|
|
"local_deep_research.web.auth.decorators.current_user",
|
|
return_value="user@domain.com",
|
|
):
|
|
result = get_user_id()
|
|
assert result == "user@domain.com"
|
|
|
|
|
|
# ============= News Feed Endpoint Tests =============
|
|
|
|
|
|
# ============= Error handler tests =============
|
|
|
|
|
|
class TestErrorHandlers:
|
|
"""Tests for error handlers."""
|
|
|
|
def test_bad_request_handler(self, app, client):
|
|
"""The 400 handler returns the documented JSON envelope, not Flask's HTML."""
|
|
from werkzeug.exceptions import BadRequest
|
|
|
|
from local_deep_research.news.flask_api import bad_request
|
|
|
|
with app.app_context():
|
|
response, status = bad_request(BadRequest())
|
|
assert status == 400
|
|
assert response.get_json() == {"error": "Bad request"}
|
|
|
|
def test_not_found_handler(self, app, client):
|
|
"""The 404 handler returns the documented JSON envelope."""
|
|
from werkzeug.exceptions import NotFound
|
|
|
|
from local_deep_research.news.flask_api import not_found
|
|
|
|
with app.app_context():
|
|
response, status = not_found(NotFound())
|
|
assert status == 404
|
|
assert response.get_json() == {"error": "Resource not found"}
|
|
|
|
def test_internal_error_handler(self, app, client):
|
|
"""The 500 handler returns the documented JSON envelope."""
|
|
from werkzeug.exceptions import InternalServerError
|
|
|
|
from local_deep_research.news.flask_api import internal_error
|
|
|
|
with app.app_context():
|
|
response, status = internal_error(InternalServerError())
|
|
assert status == 500
|
|
assert response.get_json() == {"error": "Internal server error"}
|
|
|
|
|
|
# ============= Run Subscription Now Tests =============
|