Files
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

677 lines
26 KiB
Python

"""
Coverage tests for app_factory.py targeting ~37 missing statements.
Focuses on:
- register_error_handlers: 404/500 JSON vs HTML branching, CSRF handler,
news API exception handler
- create_app: secret key file read/write branches, news scheduler
enabled/disabled paths
- apply_middleware: teardown cleanup_db_session, WebSocket handler
- create_database: deprecated no-op
- register_blueprints: CSRF exemption path
"""
import pytest
from unittest.mock import MagicMock, patch
from flask import Flask
MODULE = "local_deep_research.web.app_factory"
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _minimal_app():
"""Minimal Flask app suitable for registering error handlers."""
app = Flask(__name__)
app.config["TESTING"] = True
app.config["WTF_CSRF_ENABLED"] = False
app.config["SECRET_KEY"] = "test-secret"
return app
# ---------------------------------------------------------------------------
# 1. register_error_handlers
# ---------------------------------------------------------------------------
class TestRegisterErrorHandlers:
"""Tests for the register_error_handlers function."""
@pytest.fixture
def app_with_handlers(self):
from local_deep_research.web.app_factory import register_error_handlers
app = _minimal_app()
register_error_handlers(app)
return app
@pytest.fixture
def client(self, app_with_handlers):
app_with_handlers.config["PROPAGATE_EXCEPTIONS"] = False
# Add a route that raises to test 500 handler
@app_with_handlers.route("/boom")
def boom():
raise RuntimeError("intentional error")
# Add a plain 404 trigger for non-API path
return app_with_handlers.test_client()
def test_404_api_path_returns_json(self, client):
"""404 on /api/ path returns JSON body."""
response = client.get("/api/does-not-exist")
assert response.status_code == 404
data = response.get_json()
assert data is not None
assert "error" in data
def test_404_non_api_path_returns_text(self, client):
"""404 on a non-API path returns plain text, not JSON."""
response = client.get("/totally-missing")
assert response.status_code == 404
# Should NOT be JSON
assert response.content_type.startswith("text/")
def test_500_api_path_returns_json(self, client):
"""500 on /api/ path returns JSON body."""
# Patch the boom route under /api/
app = client.application
@app.route("/api/error-test")
def api_error():
raise RuntimeError("api boom")
response = client.get("/api/error-test")
assert response.status_code == 500
data = response.get_json()
assert data is not None
assert "error" in data
def test_500_non_api_path_returns_text(self, client):
"""500 on a non-API path returns plain text."""
response = client.get("/boom")
assert response.status_code == 500
assert response.content_type.startswith("text/")
def test_csrf_error_handler_registered(self, app_with_handlers):
"""CSRFError handler is registered when flask_wtf is available."""
try:
from flask_wtf.csrf import CSRFError
# handler should be in the error_handler_spec
spec = app_with_handlers.error_handler_spec
# Flatten all registered exception-class keys from inner handler maps
all_exc_keys = []
for blueprint_handlers in spec.values():
for code_or_exc, handler_map in blueprint_handlers.items():
# handler_map maps exception class -> handler function
if isinstance(handler_map, dict):
all_exc_keys.extend(handler_map.keys())
# CSRFError should appear somewhere in registered handlers
has_csrf = any(
(isinstance(k, type) and issubclass(k, CSRFError))
or k == CSRFError
for k in all_exc_keys
)
assert has_csrf
except ImportError:
pytest.skip("flask_wtf not available")
def test_csrf_error_handler_returns_400(self):
"""CSRF error handler returns 400 with JSON error message."""
from local_deep_research.web.app_factory import register_error_handlers
try:
from flask_wtf.csrf import CSRFProtect # noqa: F401
except ImportError:
pytest.skip("flask_wtf not available")
app = _minimal_app()
app.config["WTF_CSRF_ENABLED"] = True
app.config["SECRET_KEY"] = "test-secret-key-for-csrf"
CSRFProtect(app)
register_error_handlers(app)
# Add a POST route that requires CSRF
@app.route("/form-post", methods=["POST"])
def form_post():
return "ok"
client = app.test_client()
# Posting without a CSRF token on a CSRF-protected app triggers CSRFError
app.config["WTF_CSRF_ENABLED"] = True
response = client.post("/form-post", data={})
assert response.status_code == 400
data = response.get_json()
assert "error" in data
class TestNewsApiExceptionHandler:
"""Tests for the NewsAPIException error handler."""
def test_news_api_exception_handler_registered_when_module_available(self):
"""NewsAPIException handler is registered when news module is available."""
from local_deep_research.web.app_factory import register_error_handlers
app = _minimal_app()
register_error_handlers(app)
try:
from local_deep_research.news.exceptions import NewsAPIException
spec = app.error_handler_spec
all_exc_keys = []
for blueprint_handlers in spec.values():
for code_or_exc, handler_map in blueprint_handlers.items():
# handler_map maps exception class -> handler function
if isinstance(handler_map, dict):
all_exc_keys.extend(handler_map.keys())
has_news = any(
(isinstance(k, type) and issubclass(k, NewsAPIException))
or k == NewsAPIException
for k in all_exc_keys
)
assert has_news
except ImportError:
pytest.skip("news module not available")
def test_news_api_exception_handler_returns_json(self):
"""NewsAPIException is converted to a JSON response with correct status."""
from local_deep_research.web.app_factory import register_error_handlers
try:
from local_deep_research.news.exceptions import NewsAPIException
except ImportError:
pytest.skip("news module not available")
app = _minimal_app()
register_error_handlers(app)
@app.route("/news-error")
def news_error():
raise NewsAPIException(
message="feed unavailable",
error_code="FEED_ERROR",
status_code=503,
)
app.config["PROPAGATE_EXCEPTIONS"] = False
client = app.test_client()
response = client.get("/news-error")
assert response.status_code == 503
data = response.get_json()
assert data is not None
# ---------------------------------------------------------------------------
# 2. create_database (deprecated)
# ---------------------------------------------------------------------------
class TestCreateDatabase:
"""Tests for the deprecated create_database function."""
def test_create_database_is_noop(self):
"""create_database does nothing (deprecated)."""
from local_deep_research.web.app_factory import create_database
app = _minimal_app()
# Should return None and not raise
result = create_database(app)
assert result is None
def test_create_database_accepts_any_app(self):
"""create_database accepts any app argument without error."""
# audit: PUNCHLIST reviewed 2026-05 — KEEP (ASSERT_TRUE).
from local_deep_research.web.app_factory import create_database
create_database(None) # Even None is acceptable
create_database(Flask(__name__))
# ---------------------------------------------------------------------------
# 3. create_app: secret key branching
# ---------------------------------------------------------------------------
class TestCreateAppSecretKeyBranches:
"""Tests for the secret key file read/write branches in create_app."""
def test_secret_key_read_from_existing_file(self, tmp_path):
"""When secret key file exists, it is read and used."""
secret_file = tmp_path / ".secret_key"
secret_file.write_text("my-persisted-key")
with (
patch(MODULE + ".SocketIOService"),
patch(
"local_deep_research.config.paths.get_data_directory",
return_value=tmp_path,
),
patch("atexit.register"),
):
from local_deep_research.web.app_factory import create_app
app, _ = create_app()
assert app.config["SECRET_KEY"] == "my-persisted-key"
def test_secret_key_generated_when_file_missing(self, tmp_path):
"""When no secret key file exists, a new key is generated and saved."""
key_dir = tmp_path / "data"
key_dir.mkdir()
with (
patch(MODULE + ".SocketIOService"),
patch(
"local_deep_research.config.paths.get_data_directory",
return_value=key_dir,
),
patch("atexit.register"),
):
from local_deep_research.web.app_factory import create_app
app, _ = create_app()
# A hex key should have been generated
assert (
len(app.config["SECRET_KEY"]) == 64
) # 32 bytes -> 64 hex chars
# The key file should have been written
assert (key_dir / ".secret_key").exists()
def test_secret_key_fallback_when_file_read_fails(self, tmp_path):
"""When secret key file exists but cannot be read, a fresh key is used."""
secret_file = tmp_path / ".secret_key"
secret_file.write_text("original-key")
_real_open = open
def _open_raises_for_secret_key(path, *args, **kwargs):
"""Raise OSError only for the .secret_key file; delegate otherwise."""
if str(path).endswith(".secret_key"):
raise OSError("permission denied")
return _real_open(path, *args, **kwargs)
with (
patch(MODULE + ".SocketIOService"),
patch(
"local_deep_research.config.paths.get_data_directory",
return_value=tmp_path,
),
patch("atexit.register"),
patch("builtins.open", side_effect=_open_raises_for_secret_key),
):
from local_deep_research.web.app_factory import create_app
app, _ = create_app()
# Should still have a secret key (fallback generated)
assert app.config["SECRET_KEY"] is not None
assert len(app.config["SECRET_KEY"]) > 0
# ---------------------------------------------------------------------------
# 4. create_app: news scheduler branching
# ---------------------------------------------------------------------------
class TestCreateAppNewsScheduler:
"""Tests for the news scheduler enabled/disabled branches."""
def test_news_scheduler_disabled_sets_none(self):
"""When news scheduler is disabled, app.background_job_scheduler is None."""
with (
patch(MODULE + ".SocketIOService"),
patch(MODULE + ".apply_middleware"),
patch(MODULE + ".register_blueprints"),
patch(MODULE + ".register_error_handlers"),
patch("atexit.register"),
patch(
"local_deep_research.settings.env_registry.get_env_setting",
return_value=False,
),
):
from local_deep_research.web.app_factory import create_app
app, _ = create_app()
assert app.background_job_scheduler is None
def test_news_scheduler_exception_sets_none(self):
"""When scheduler init throws, app.background_job_scheduler is None."""
with (
patch(MODULE + ".SocketIOService"),
patch(MODULE + ".apply_middleware"),
patch(MODULE + ".register_blueprints"),
patch(MODULE + ".register_error_handlers"),
patch("atexit.register"),
patch(
"local_deep_research.settings.env_registry.get_env_setting",
return_value=True,
),
patch(
"local_deep_research.scheduler.background.get_background_job_scheduler",
side_effect=RuntimeError("scheduler boom"),
),
):
from local_deep_research.web.app_factory import create_app
app, _ = create_app()
assert app.background_job_scheduler is None
class TestCreateAppQueueProcessor:
"""Tests for queue processor startup gating."""
@staticmethod
def _stub_queue_processor(monkeypatch):
"""Install a lightweight queue processor stub for app_factory imports."""
import sys
import types
mock_queue_processor = MagicMock()
queue_pkg = types.ModuleType("local_deep_research.web.queue")
queue_pkg.__path__ = []
processor_mod = types.ModuleType(
"local_deep_research.web.queue.processor_v2"
)
setattr(processor_mod, "queue_processor", mock_queue_processor)
monkeypatch.setitem(
sys.modules, "local_deep_research.web.queue", queue_pkg
)
monkeypatch.setitem(
sys.modules,
"local_deep_research.web.queue.processor_v2",
processor_mod,
)
return mock_queue_processor
def test_queue_processor_env_setting_is_registered(self):
"""The queue processor has an env-only setting operators can flip."""
from local_deep_research.settings.env_registry import registry
assert (
registry.get_env_var("web.queue_processor.enabled")
== "LDR_WEB_QUEUE_PROCESSOR_ENABLED"
)
def test_queue_processor_disabled_during_pytest(self, monkeypatch):
"""Real pytest app startup does not import or start the queue processor."""
import sys
monkeypatch.setenv(
"PYTEST_CURRENT_TEST", "tests/web/test_app_factory.py::test_app"
)
monkeypatch.setenv("LDR_NEWS_SCHEDULER_ENABLED", "false")
monkeypatch.delenv("LDR_WEB_QUEUE_PROCESSOR_ENABLED", raising=False)
for module_name in (
"local_deep_research.web.auth.queue_middleware",
"local_deep_research.web.queue.processor_v2",
):
monkeypatch.delitem(sys.modules, module_name, raising=False)
with (
patch(MODULE + ".SocketIOService"),
patch(MODULE + ".register_blueprints"),
patch(MODULE + ".register_error_handlers"),
patch("atexit.register"),
):
from local_deep_research.web.app_factory import create_app
create_app()
assert "local_deep_research.web.queue.processor_v2" not in sys.modules
def test_queue_processor_can_opt_in_under_pytest(self, monkeypatch):
"""Tests that need the real queue processor can opt in explicitly."""
monkeypatch.setenv(
"PYTEST_CURRENT_TEST", "tests/web/test_app_factory.py::test_app"
)
monkeypatch.setenv("LDR_NEWS_SCHEDULER_ENABLED", "false")
monkeypatch.setenv("LDR_WEB_QUEUE_PROCESSOR_ENABLED", "true")
mock_queue_processor = self._stub_queue_processor(monkeypatch)
with (
patch(MODULE + ".SocketIOService"),
patch(MODULE + ".apply_middleware"),
patch(MODULE + ".register_blueprints"),
patch(MODULE + ".register_error_handlers"),
patch("atexit.register"),
):
from local_deep_research.web.app_factory import create_app
create_app()
mock_queue_processor.start.assert_called_once_with()
def test_queue_processor_still_starts_when_only_ci_is_set(
self, monkeypatch
):
"""CI alone is not treated as test-mode opt-out for the queue."""
monkeypatch.setenv("CI", "true")
monkeypatch.setenv("LDR_NEWS_SCHEDULER_ENABLED", "false")
monkeypatch.delenv("TESTING", raising=False)
monkeypatch.delenv("PYTEST_CURRENT_TEST", raising=False)
monkeypatch.delenv("LDR_TESTING_TEST_MODE", raising=False)
monkeypatch.delenv("LDR_WEB_QUEUE_PROCESSOR_ENABLED", raising=False)
mock_queue_processor = self._stub_queue_processor(monkeypatch)
with (
patch(MODULE + ".SocketIOService"),
patch(MODULE + ".apply_middleware"),
patch(MODULE + ".register_blueprints"),
patch(MODULE + ".register_error_handlers"),
patch("atexit.register"),
):
from local_deep_research.web.app_factory import create_app
create_app()
mock_queue_processor.start.assert_called_once_with()
def test_queue_processor_still_starts_when_testing_env_is_false(
self, monkeypatch
):
"""TESTING=false is parsed as false, not as an opt-out signal."""
monkeypatch.setenv("TESTING", "false")
monkeypatch.setenv("LDR_NEWS_SCHEDULER_ENABLED", "false")
monkeypatch.delenv("PYTEST_CURRENT_TEST", raising=False)
monkeypatch.delenv("LDR_TESTING_TEST_MODE", raising=False)
monkeypatch.delenv("LDR_WEB_QUEUE_PROCESSOR_ENABLED", raising=False)
mock_queue_processor = self._stub_queue_processor(monkeypatch)
with (
patch(MODULE + ".SocketIOService"),
patch(MODULE + ".apply_middleware"),
patch(MODULE + ".register_blueprints"),
patch(MODULE + ".register_error_handlers"),
patch("atexit.register"),
):
from local_deep_research.web.app_factory import create_app
create_app()
mock_queue_processor.start.assert_called_once_with()
def test_queue_processor_still_starts_when_testing_env_is_true(
self, monkeypatch
):
"""TESTING=true is a dev-server mode, not a pytest opt-out signal."""
monkeypatch.setenv("TESTING", "true")
monkeypatch.setenv("LDR_NEWS_SCHEDULER_ENABLED", "false")
monkeypatch.delenv("PYTEST_CURRENT_TEST", raising=False)
monkeypatch.delenv("LDR_TESTING_TEST_MODE", raising=False)
monkeypatch.delenv("LDR_WEB_QUEUE_PROCESSOR_ENABLED", raising=False)
mock_queue_processor = self._stub_queue_processor(monkeypatch)
with (
patch(MODULE + ".SocketIOService"),
patch(MODULE + ".apply_middleware"),
patch(MODULE + ".register_blueprints"),
patch(MODULE + ".register_error_handlers"),
patch("atexit.register"),
):
from local_deep_research.web.app_factory import create_app
create_app()
mock_queue_processor.start.assert_called_once_with()
def test_queue_processor_starts_with_runtime_test_mode(self, monkeypatch):
"""Runtime concurrency test mode does not disable queue processing."""
monkeypatch.setenv("LDR_TESTING_TEST_MODE", "true")
monkeypatch.setenv("LDR_NEWS_SCHEDULER_ENABLED", "false")
monkeypatch.delenv("PYTEST_CURRENT_TEST", raising=False)
monkeypatch.delenv("LDR_WEB_QUEUE_PROCESSOR_ENABLED", raising=False)
mock_queue_processor = self._stub_queue_processor(monkeypatch)
with (
patch(MODULE + ".SocketIOService"),
patch(MODULE + ".apply_middleware"),
patch(MODULE + ".register_blueprints"),
patch(MODULE + ".register_error_handlers"),
patch("atexit.register"),
):
from local_deep_research.web.app_factory import create_app
create_app()
mock_queue_processor.start.assert_called_once_with()
# ---------------------------------------------------------------------------
# 5. apply_middleware: teardown and WebSocket handler
# ---------------------------------------------------------------------------
class TestApplyMiddlewareTeardown:
"""Tests for cleanup_db_session teardown and WebSocket before_request."""
@pytest.fixture
def app_with_middleware(self):
"""Create a full app and return it for request-context testing."""
with (
patch(MODULE + ".SocketIOService"),
patch("atexit.register"),
):
from local_deep_research.web.app_factory import create_app
app, _ = create_app()
app.config["TESTING"] = True
app.config["WTF_CSRF_ENABLED"] = False
return app
def test_teardown_runs_without_db_session(self, app_with_middleware):
"""Teardown context function runs cleanly when no db_session in g."""
# audit: PUNCHLIST reviewed 2026-05 — KEEP (ASSERT_TRUE).
with app_with_middleware.test_request_context("/"):
# Push and pop app context; teardown should not raise
with app_with_middleware.app_context():
pass # teardown fires on exit
def test_teardown_closes_session_on_exception(self, app_with_middleware):
"""Teardown rolls back and closes a session stored in g."""
from flask import g
mock_session = MagicMock()
with app_with_middleware.test_request_context("/"):
with app_with_middleware.app_context():
g.db_session = mock_session
# After app context exits, teardown ran; verify session was closed
mock_session.rollback.assert_called_once()
mock_session.close.assert_called_once()
def test_websocket_path_returns_none_when_no_socket(
self, app_with_middleware
):
"""Request to /socket.io without werkzeug.socket returns None (continue)."""
client = app_with_middleware.test_client()
# The before_request handler for /socket.io returns early without error
# A GET to /socket.io/... may 404 but should not 500
response = client.get("/socket.io/test")
# Either 404 or any non-500 is acceptable
assert response.status_code != 500
# ---------------------------------------------------------------------------
# 6. register_blueprints: CSRF exemption branch
# ---------------------------------------------------------------------------
class TestRegisterBlueprintsCsrfExemption:
"""Tests for the CSRF exemption block in register_blueprints."""
def test_csrf_exemption_applied_to_api_v1(self):
"""api_v1 blueprint is exempted from CSRF when extension is present."""
with (
patch(MODULE + ".SocketIOService"),
patch("atexit.register"),
):
from local_deep_research.web.app_factory import create_app
app, _ = create_app()
if "csrf" not in app.extensions:
pytest.skip("CSRF extension not registered")
csrf = app.extensions["csrf"]
api_v1_bp = app.blueprints.get("api_v1")
if api_v1_bp is None:
pytest.skip("api_v1 blueprint not registered")
# The exemption list should include the api_v1 blueprint
assert api_v1_bp in csrf._exempt_blueprints
def test_csrf_extension_absent_skips_exemption_gracefully(self):
"""When csrf extension is absent, CSRF exemption block is skipped."""
from local_deep_research.web.app_factory import register_blueprints
app = _minimal_app()
# Remove csrf from extensions (if present)
app.extensions.pop("csrf", None)
# register_blueprints should not raise even without csrf
# (Requires minimal blueprint setup; we just verify no AttributeError)
try:
register_blueprints(app)
except Exception as exc:
# Blueprint import errors are acceptable; csrf-related ones are not
if "csrf" in str(exc).lower():
pytest.fail(f"Unexpected csrf-related error: {exc}")
# ---------------------------------------------------------------------------
# 7. apply_middleware: register_blueprints static/favicon routes
# ---------------------------------------------------------------------------
class TestStaticAndFaviconRoutes:
"""Tests for static file and favicon routes registered in register_blueprints."""
@pytest.fixture
def app(self):
with (
patch(MODULE + ".SocketIOService"),
patch("atexit.register"),
):
from local_deep_research.web.app_factory import create_app
app, _ = create_app()
app.config["TESTING"] = True
app.config["WTF_CSRF_ENABLED"] = False
return app
def test_favicon_route_registered(self, app):
"""favicon.ico route is registered."""
rules = [rule.rule for rule in app.url_map.iter_rules()]
assert "/favicon.ico" in rules
def test_static_path_route_registered(self, app):
"""Static file path route /static/<path:path> is registered."""
rules = [rule.rule for rule in app.url_map.iter_rules()]
assert any("/static/" in r for r in rules)
def test_static_route_returns_404_for_missing_file(self, app):
"""Static route returns 404 when file does not exist."""
client = app.test_client()
response = client.get("/static/definitely-not-there.js")
assert response.status_code == 404