chore: import upstream snapshot with attribution
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
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
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""Tests for web utility modules."""
|
||||
@@ -0,0 +1,34 @@
|
||||
"""Fixtures for web utility tests."""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_flask_app():
|
||||
"""Create a mock Flask application."""
|
||||
app = Mock()
|
||||
app.debug = False
|
||||
app.config = {}
|
||||
app.jinja_env = Mock()
|
||||
app.jinja_env.globals = {}
|
||||
return app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_flask_app_debug():
|
||||
"""Create a mock Flask application in debug mode."""
|
||||
app = Mock()
|
||||
app.debug = True
|
||||
app.config = {}
|
||||
app.jinja_env = Mock()
|
||||
app.jinja_env.globals = {}
|
||||
return app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_request():
|
||||
"""Create a mock Flask request."""
|
||||
request = Mock()
|
||||
request.environ = {}
|
||||
return request
|
||||
@@ -0,0 +1,96 @@
|
||||
"""
|
||||
Tests for get_client_ip() from security/rate_limiter.py
|
||||
|
||||
Tests cover:
|
||||
- X-Forwarded-For header parsing (single IP, multiple IPs, whitespace stripping)
|
||||
- X-Real-IP header parsing (presence, whitespace stripping)
|
||||
- Header priority (X-Forwarded-For over X-Real-IP)
|
||||
- Fallback to get_remote_address() when no proxy headers present
|
||||
"""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from flask import Flask
|
||||
|
||||
from local_deep_research.security.rate_limiter import get_client_ip
|
||||
|
||||
|
||||
def _make_app():
|
||||
"""Create a minimal Flask test app."""
|
||||
app = Flask(__name__)
|
||||
app.config["TESTING"] = True
|
||||
return app
|
||||
|
||||
|
||||
class TestGetClientIp:
|
||||
"""Tests for get_client_ip function."""
|
||||
|
||||
def test_returns_single_ip_from_x_forwarded_for(self):
|
||||
"""Should return the IP when X-Forwarded-For contains a single IP."""
|
||||
app = _make_app()
|
||||
with app.test_request_context(
|
||||
environ_base={"HTTP_X_FORWARDED_FOR": "1.2.3.4"}
|
||||
):
|
||||
result = get_client_ip()
|
||||
assert result == "1.2.3.4"
|
||||
|
||||
def test_returns_first_ip_from_x_forwarded_for_with_multiple(self):
|
||||
"""Should return the first IP when X-Forwarded-For has comma-separated IPs."""
|
||||
app = _make_app()
|
||||
with app.test_request_context(
|
||||
environ_base={
|
||||
"HTTP_X_FORWARDED_FOR": "1.2.3.4, 5.6.7.8, 9.10.11.12"
|
||||
}
|
||||
):
|
||||
result = get_client_ip()
|
||||
assert result == "1.2.3.4"
|
||||
|
||||
def test_strips_whitespace_from_x_forwarded_for(self):
|
||||
"""Should strip leading/trailing whitespace from X-Forwarded-For IP."""
|
||||
app = _make_app()
|
||||
with app.test_request_context(
|
||||
environ_base={"HTTP_X_FORWARDED_FOR": " 1.2.3.4 , 5.6.7.8"}
|
||||
):
|
||||
result = get_client_ip()
|
||||
assert result == "1.2.3.4"
|
||||
|
||||
def test_returns_x_real_ip_when_no_x_forwarded_for(self):
|
||||
"""Should return X-Real-IP when X-Forwarded-For is absent."""
|
||||
app = _make_app()
|
||||
with app.test_request_context(
|
||||
environ_base={"HTTP_X_REAL_IP": "10.20.30.40"}
|
||||
):
|
||||
result = get_client_ip()
|
||||
assert result == "10.20.30.40"
|
||||
|
||||
def test_strips_whitespace_from_x_real_ip(self):
|
||||
"""Should strip leading/trailing whitespace from X-Real-IP."""
|
||||
app = _make_app()
|
||||
with app.test_request_context(
|
||||
environ_base={"HTTP_X_REAL_IP": " 10.20.30.40 "}
|
||||
):
|
||||
result = get_client_ip()
|
||||
assert result == "10.20.30.40"
|
||||
|
||||
def test_x_forwarded_for_takes_priority_over_x_real_ip(self):
|
||||
"""X-Forwarded-For should take priority when both headers are present."""
|
||||
app = _make_app()
|
||||
with app.test_request_context(
|
||||
environ_base={
|
||||
"HTTP_X_FORWARDED_FOR": "192.168.1.1",
|
||||
"HTTP_X_REAL_IP": "10.0.0.1",
|
||||
}
|
||||
):
|
||||
result = get_client_ip()
|
||||
assert result == "192.168.1.1"
|
||||
|
||||
def test_falls_back_to_get_remote_address(self):
|
||||
"""Should fall back to get_remote_address() when no proxy headers present."""
|
||||
app = _make_app()
|
||||
with app.test_request_context():
|
||||
with patch(
|
||||
"local_deep_research.security.rate_limiter.get_remote_address",
|
||||
return_value="127.0.0.1",
|
||||
):
|
||||
result = get_client_ip()
|
||||
assert result == "127.0.0.1"
|
||||
@@ -0,0 +1,98 @@
|
||||
"""Tests for web/utils/request_helpers.py.
|
||||
|
||||
Locks in parse_bool_arg's behavior contract: it MUST behave identically to
|
||||
the inline pattern ``request.args.get(name, "false_or_true").lower() == "true"``
|
||||
that it replaces. In particular: no whitespace stripping, no widening of the
|
||||
truthy set beyond the literal string ``"true"`` (case-insensitive).
|
||||
"""
|
||||
|
||||
from flask import Flask
|
||||
|
||||
from local_deep_research.web.utils.request_helpers import parse_bool_arg
|
||||
|
||||
|
||||
def _make_app():
|
||||
"""Create a minimal Flask test app."""
|
||||
app = Flask(__name__)
|
||||
app.config["TESTING"] = True
|
||||
return app
|
||||
|
||||
|
||||
class TestParseBoolArg:
|
||||
"""Tests for parse_bool_arg."""
|
||||
|
||||
def test_missing_param_returns_default_false(self):
|
||||
"""Absent param returns the default (False)."""
|
||||
app = _make_app()
|
||||
with app.test_request_context("/?other=value"):
|
||||
assert parse_bool_arg("flag") is False
|
||||
|
||||
def test_missing_param_returns_default_true(self):
|
||||
"""Absent param returns the default (True) when default=True."""
|
||||
app = _make_app()
|
||||
with app.test_request_context("/?other=value"):
|
||||
assert parse_bool_arg("flag", default=True) is True
|
||||
|
||||
def test_value_true_lowercase(self):
|
||||
"""?flag=true returns True."""
|
||||
app = _make_app()
|
||||
with app.test_request_context("/?flag=true"):
|
||||
assert parse_bool_arg("flag") is True
|
||||
|
||||
def test_value_true_uppercase(self):
|
||||
"""?flag=TRUE returns True (case-insensitive)."""
|
||||
app = _make_app()
|
||||
with app.test_request_context("/?flag=TRUE"):
|
||||
assert parse_bool_arg("flag") is True
|
||||
|
||||
def test_value_true_mixed_case(self):
|
||||
"""?flag=True returns True (case-insensitive)."""
|
||||
app = _make_app()
|
||||
with app.test_request_context("/?flag=True"):
|
||||
assert parse_bool_arg("flag") is True
|
||||
|
||||
def test_value_false(self):
|
||||
"""?flag=false returns False."""
|
||||
app = _make_app()
|
||||
with app.test_request_context("/?flag=false"):
|
||||
assert parse_bool_arg("flag", default=True) is False
|
||||
|
||||
def test_empty_value_returns_false(self):
|
||||
"""?flag= (empty string) returns False, NOT the default.
|
||||
|
||||
This matches the inline pattern: ``request.args.get("flag", "false")``
|
||||
returns the empty string when the param is present-but-empty, not the
|
||||
default. The empty string lowercases to "" which != "true", so False.
|
||||
"""
|
||||
app = _make_app()
|
||||
with app.test_request_context("/?flag="):
|
||||
# Even with default=True, an empty value still returns False.
|
||||
assert parse_bool_arg("flag", default=True) is False
|
||||
|
||||
def test_value_yes_returns_false(self):
|
||||
"""?flag=yes returns False — only "true" is truthy."""
|
||||
app = _make_app()
|
||||
with app.test_request_context("/?flag=yes"):
|
||||
assert parse_bool_arg("flag") is False
|
||||
|
||||
def test_value_one_returns_false(self):
|
||||
"""?flag=1 returns False — only "true" is truthy."""
|
||||
app = _make_app()
|
||||
with app.test_request_context("/?flag=1"):
|
||||
assert parse_bool_arg("flag") is False
|
||||
|
||||
def test_value_on_returns_false(self):
|
||||
"""?flag=on returns False — only "true" is truthy."""
|
||||
app = _make_app()
|
||||
with app.test_request_context("/?flag=on"):
|
||||
assert parse_bool_arg("flag") is False
|
||||
|
||||
def test_whitespace_padded_true_returns_false(self):
|
||||
"""?flag=%20true%20 returns False — whitespace is NOT stripped.
|
||||
|
||||
This locks in the no-strip behavior: a stripping helper would return
|
||||
True here, which would diverge from the original inline pattern.
|
||||
"""
|
||||
app = _make_app()
|
||||
with app.test_request_context("/?flag=%20true%20"):
|
||||
assert parse_bool_arg("flag") is False
|
||||
@@ -0,0 +1,186 @@
|
||||
"""Tests for the CI request-timing forensics middleware (#4431)."""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from local_deep_research.web.utils.request_timing import (
|
||||
FREEZE_DUMP_SECONDS,
|
||||
RequestTimingMiddleware,
|
||||
SLOW_REQUEST_SECONDS,
|
||||
_should_arm_freeze_dump as _orig_should_arm_freeze_dump,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _mock_faulthandler(monkeypatch):
|
||||
"""Replace faulthandler so tests never arm a real repeating thread-dump
|
||||
timer (which would fire on stderr mid-suite), and force the pytest gate
|
||||
OPEN so the arming path is actually exercised. Returns the mock so a
|
||||
test can assert the dead-man's switch was armed."""
|
||||
from local_deep_research.web.utils import request_timing as mod
|
||||
|
||||
fake = MagicMock()
|
||||
monkeypatch.setattr(mod, "faulthandler", fake)
|
||||
# _should_arm_freeze_dump() returns False under pytest; override so the
|
||||
# arming path runs in these tests.
|
||||
monkeypatch.setattr(mod, "_should_arm_freeze_dump", lambda: True)
|
||||
return fake
|
||||
|
||||
|
||||
def _make_environ(method="GET", path="/chat/", query=""):
|
||||
return {
|
||||
"REQUEST_METHOD": method,
|
||||
"PATH_INFO": path,
|
||||
"QUERY_STRING": query,
|
||||
}
|
||||
|
||||
|
||||
def _collect_logs(monkeypatch):
|
||||
"""Capture loguru records emitted by the middleware module."""
|
||||
records = []
|
||||
from local_deep_research.web.utils import request_timing as mod
|
||||
|
||||
class FakeLogger:
|
||||
def info(self, msg):
|
||||
records.append(("info", msg))
|
||||
|
||||
def warning(self, msg):
|
||||
records.append(("warning", msg))
|
||||
|
||||
def debug(self, msg):
|
||||
records.append(("debug", msg))
|
||||
|
||||
monkeypatch.setattr(mod, "logger", FakeLogger())
|
||||
return records
|
||||
|
||||
|
||||
def test_logs_arrival_and_completion(monkeypatch):
|
||||
records = _collect_logs(monkeypatch)
|
||||
|
||||
def app(environ, start_response):
|
||||
return [b"ok"]
|
||||
|
||||
mw = RequestTimingMiddleware(app)
|
||||
body = mw(_make_environ(), lambda *a: None)
|
||||
|
||||
assert body == [b"ok"]
|
||||
assert records[0] == ("info", "[req] > GET /chat/")
|
||||
level, msg = records[1]
|
||||
assert level == "info"
|
||||
assert msg.startswith("[req] < GET /chat/")
|
||||
|
||||
|
||||
def test_slow_request_logged_as_warning(monkeypatch):
|
||||
records = _collect_logs(monkeypatch)
|
||||
from local_deep_research.web.utils import request_timing as mod
|
||||
|
||||
# Simulate a slow WSGI call without sleeping: jump the clock by
|
||||
# patching time.monotonic to advance past the slow threshold.
|
||||
ticks = iter([100.0, 100.0 + SLOW_REQUEST_SECONDS + 1])
|
||||
monkeypatch.setattr(mod.time, "monotonic", lambda: next(ticks))
|
||||
|
||||
mw = RequestTimingMiddleware(lambda e, s: [b"ok"])
|
||||
mw(_make_environ(path="/slow"), lambda *a: None)
|
||||
|
||||
level, msg = records[-1]
|
||||
assert level == "warning"
|
||||
assert "SLOW" in msg
|
||||
|
||||
|
||||
def test_socketio_paths_keep_query_string(monkeypatch):
|
||||
records = _collect_logs(monkeypatch)
|
||||
|
||||
mw = RequestTimingMiddleware(lambda e, s: [b"ok"])
|
||||
mw(
|
||||
_make_environ(path="/socket.io/", query="transport=polling&sid=abc"),
|
||||
lambda *a: None,
|
||||
)
|
||||
|
||||
assert records[0] == (
|
||||
"info",
|
||||
"[req] > GET /socket.io/?transport=polling&sid=abc",
|
||||
)
|
||||
|
||||
|
||||
def test_arrival_logged_even_when_app_raises(monkeypatch):
|
||||
records = _collect_logs(monkeypatch)
|
||||
|
||||
def exploding_app(environ, start_response):
|
||||
raise RuntimeError("boom")
|
||||
|
||||
mw = RequestTimingMiddleware(exploding_app)
|
||||
try:
|
||||
mw(_make_environ(path="/boom"), lambda *a: None)
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
assert records[0] == ("info", "[req] > GET /boom")
|
||||
# completion line still emitted by the finally block
|
||||
assert any("[req] < GET /boom" in msg for _, msg in records[1:])
|
||||
|
||||
|
||||
def test_arms_freeze_dump_on_init_and_rearms_per_request(
|
||||
monkeypatch, _mock_faulthandler
|
||||
):
|
||||
_collect_logs(monkeypatch)
|
||||
|
||||
# Construction arms the dead-man's switch.
|
||||
mw = RequestTimingMiddleware(lambda e, s: [b"ok"])
|
||||
_mock_faulthandler.enable.assert_called_once()
|
||||
assert _mock_faulthandler.dump_traceback_later.call_count == 1
|
||||
_, kwargs = _mock_faulthandler.dump_traceback_later.call_args
|
||||
assert _mock_faulthandler.dump_traceback_later.call_args.args[0] == (
|
||||
FREEZE_DUMP_SECONDS
|
||||
)
|
||||
assert kwargs.get("repeat") is True
|
||||
|
||||
# Each request re-arms it (resets the countdown).
|
||||
mw(_make_environ(), lambda *a: None)
|
||||
assert _mock_faulthandler.dump_traceback_later.call_count == 2
|
||||
|
||||
|
||||
def test_path_crlf_is_sanitized_against_log_injection(monkeypatch):
|
||||
records = _collect_logs(monkeypatch)
|
||||
|
||||
mw = RequestTimingMiddleware(lambda e, s: [b"ok"])
|
||||
mw(_make_environ(path="/evil\n[req] > GET /forged"), lambda *a: None)
|
||||
|
||||
arrival = records[0][1]
|
||||
assert "\n" not in arrival
|
||||
assert "\\n" in arrival
|
||||
assert arrival == "[req] > GET /evil\\n[req] > GET /forged"
|
||||
|
||||
|
||||
def test_freeze_dump_failure_never_breaks_requests(monkeypatch):
|
||||
"""If faulthandler raises, the request must still be served."""
|
||||
from local_deep_research.web.utils import request_timing as mod
|
||||
|
||||
boom = MagicMock()
|
||||
boom.enable.side_effect = RuntimeError("no faulthandler")
|
||||
boom.dump_traceback_later.side_effect = RuntimeError("nope")
|
||||
monkeypatch.setattr(mod, "faulthandler", boom)
|
||||
monkeypatch.setattr(mod, "_should_arm_freeze_dump", lambda: True)
|
||||
_collect_logs(monkeypatch)
|
||||
|
||||
mw = RequestTimingMiddleware(lambda e, s: [b"served"])
|
||||
assert mw(_make_environ(), lambda *a: None) == [b"served"]
|
||||
|
||||
|
||||
def test_freeze_dump_not_armed_under_pytest(monkeypatch):
|
||||
"""The dead-man's switch must be a no-op under pytest so it doesn't spew
|
||||
dumps across the test run; only the real server arms it."""
|
||||
from local_deep_research.web.utils import request_timing as mod
|
||||
|
||||
fake = MagicMock()
|
||||
monkeypatch.setattr(mod, "faulthandler", fake)
|
||||
# Undo the autouse fixture's gate override; exercise the REAL gate,
|
||||
# which sees pytest in sys.modules and must suppress arming.
|
||||
monkeypatch.setattr(
|
||||
mod, "_should_arm_freeze_dump", _orig_should_arm_freeze_dump
|
||||
)
|
||||
assert mod._should_arm_freeze_dump() is False
|
||||
mw = RequestTimingMiddleware(lambda e, s: [b"ok"])
|
||||
mw(_make_environ(), lambda *a: None)
|
||||
fake.dump_traceback_later.assert_not_called()
|
||||
fake.enable.assert_not_called()
|
||||
@@ -0,0 +1,151 @@
|
||||
"""Tests for web/utils/templates.py."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
class TestRenderTemplateWithDefaults:
|
||||
"""Tests for render_template_with_defaults function."""
|
||||
|
||||
def test_adds_version_to_kwargs(self):
|
||||
"""Test that version is added to template kwargs."""
|
||||
with patch(
|
||||
"local_deep_research.web.utils.templates.render_template"
|
||||
) as mock_render:
|
||||
with patch(
|
||||
"local_deep_research.database.encrypted_db.db_manager"
|
||||
) as mock_db:
|
||||
mock_db.has_encryption = False
|
||||
mock_render.return_value = "rendered"
|
||||
|
||||
from local_deep_research.web.utils.templates import (
|
||||
render_template_with_defaults,
|
||||
)
|
||||
|
||||
render_template_with_defaults("test.html")
|
||||
|
||||
# Check that version was passed
|
||||
call_kwargs = mock_render.call_args[1]
|
||||
assert "version" in call_kwargs
|
||||
|
||||
def test_adds_has_encryption_to_kwargs(self):
|
||||
"""Test that has_encryption is added to template kwargs."""
|
||||
with patch(
|
||||
"local_deep_research.web.utils.templates.render_template"
|
||||
) as mock_render:
|
||||
with patch(
|
||||
"local_deep_research.database.encrypted_db.db_manager"
|
||||
) as mock_db:
|
||||
mock_db.has_encryption = True
|
||||
mock_render.return_value = "rendered"
|
||||
|
||||
from local_deep_research.web.utils.templates import (
|
||||
render_template_with_defaults,
|
||||
)
|
||||
|
||||
render_template_with_defaults("test.html")
|
||||
|
||||
call_kwargs = mock_render.call_args[1]
|
||||
assert call_kwargs["has_encryption"] is True
|
||||
|
||||
def test_passes_encryption_false_when_disabled(self):
|
||||
"""Test that has_encryption is False when encryption disabled."""
|
||||
with patch(
|
||||
"local_deep_research.web.utils.templates.render_template"
|
||||
) as mock_render:
|
||||
with patch(
|
||||
"local_deep_research.database.encrypted_db.db_manager"
|
||||
) as mock_db:
|
||||
mock_db.has_encryption = False
|
||||
mock_render.return_value = "rendered"
|
||||
|
||||
from local_deep_research.web.utils.templates import (
|
||||
render_template_with_defaults,
|
||||
)
|
||||
|
||||
render_template_with_defaults("test.html")
|
||||
|
||||
call_kwargs = mock_render.call_args[1]
|
||||
assert call_kwargs["has_encryption"] is False
|
||||
|
||||
def test_passes_template_name_as_first_arg(self):
|
||||
"""Test that template name is passed as first argument."""
|
||||
with patch(
|
||||
"local_deep_research.web.utils.templates.render_template"
|
||||
) as mock_render:
|
||||
with patch(
|
||||
"local_deep_research.database.encrypted_db.db_manager"
|
||||
) as mock_db:
|
||||
mock_db.has_encryption = False
|
||||
mock_render.return_value = "rendered"
|
||||
|
||||
from local_deep_research.web.utils.templates import (
|
||||
render_template_with_defaults,
|
||||
)
|
||||
|
||||
render_template_with_defaults("my_template.html")
|
||||
|
||||
call_args = mock_render.call_args[0]
|
||||
assert call_args[0] == "my_template.html"
|
||||
|
||||
def test_passes_custom_kwargs_to_render_template(self):
|
||||
"""Test that custom kwargs are passed through to render_template."""
|
||||
with patch(
|
||||
"local_deep_research.web.utils.templates.render_template"
|
||||
) as mock_render:
|
||||
with patch(
|
||||
"local_deep_research.database.encrypted_db.db_manager"
|
||||
) as mock_db:
|
||||
mock_db.has_encryption = False
|
||||
mock_render.return_value = "rendered"
|
||||
|
||||
from local_deep_research.web.utils.templates import (
|
||||
render_template_with_defaults,
|
||||
)
|
||||
|
||||
render_template_with_defaults(
|
||||
"test.html", title="My Title", data={"key": "value"}
|
||||
)
|
||||
|
||||
call_kwargs = mock_render.call_args[1]
|
||||
assert call_kwargs["title"] == "My Title"
|
||||
assert call_kwargs["data"] == {"key": "value"}
|
||||
|
||||
def test_returns_render_template_result(self):
|
||||
"""Test that the function returns the result from render_template."""
|
||||
with patch(
|
||||
"local_deep_research.web.utils.templates.render_template"
|
||||
) as mock_render:
|
||||
with patch(
|
||||
"local_deep_research.database.encrypted_db.db_manager"
|
||||
) as mock_db:
|
||||
mock_db.has_encryption = False
|
||||
mock_render.return_value = "<html>Content</html>"
|
||||
|
||||
from local_deep_research.web.utils.templates import (
|
||||
render_template_with_defaults,
|
||||
)
|
||||
|
||||
result = render_template_with_defaults("test.html")
|
||||
|
||||
assert result == "<html>Content</html>"
|
||||
|
||||
def test_passes_multiple_positional_args(self):
|
||||
"""Test that multiple positional arguments are passed through."""
|
||||
with patch(
|
||||
"local_deep_research.web.utils.templates.render_template"
|
||||
) as mock_render:
|
||||
with patch(
|
||||
"local_deep_research.database.encrypted_db.db_manager"
|
||||
) as mock_db:
|
||||
mock_db.has_encryption = False
|
||||
mock_render.return_value = "rendered"
|
||||
|
||||
from local_deep_research.web.utils.templates import (
|
||||
render_template_with_defaults,
|
||||
)
|
||||
|
||||
render_template_with_defaults("template.html", "extra_arg")
|
||||
|
||||
call_args = mock_render.call_args[0]
|
||||
assert "template.html" in call_args
|
||||
assert "extra_arg" in call_args
|
||||
@@ -0,0 +1,148 @@
|
||||
"""Tests for web/utils/theme_helper.py."""
|
||||
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
|
||||
class TestThemeHelperInit:
|
||||
"""Tests for ThemeHelper initialization."""
|
||||
|
||||
def test_init_without_app(self):
|
||||
"""Test initialization without Flask app."""
|
||||
from local_deep_research.web.utils.theme_helper import ThemeHelper
|
||||
|
||||
helper = ThemeHelper()
|
||||
assert helper.app is None
|
||||
|
||||
def test_init_with_app_calls_init_app(self):
|
||||
"""Test that initialization with app calls init_app."""
|
||||
from local_deep_research.web.utils.theme_helper import ThemeHelper
|
||||
|
||||
mock_app = Mock()
|
||||
mock_app.jinja_env = Mock()
|
||||
mock_app.jinja_env.globals = {}
|
||||
|
||||
with patch.object(ThemeHelper, "init_app") as mock_init:
|
||||
ThemeHelper(app=mock_app)
|
||||
mock_init.assert_called_once_with(mock_app)
|
||||
|
||||
|
||||
class TestThemeHelperInitApp:
|
||||
"""Tests for ThemeHelper.init_app method."""
|
||||
|
||||
def test_init_app_sets_app_attribute(self):
|
||||
"""Test that init_app sets the app attribute."""
|
||||
from local_deep_research.web.utils.theme_helper import ThemeHelper
|
||||
|
||||
helper = ThemeHelper()
|
||||
mock_app = Mock()
|
||||
mock_app.jinja_env = Mock()
|
||||
mock_app.jinja_env.globals = {}
|
||||
|
||||
helper.init_app(mock_app)
|
||||
assert helper.app is mock_app
|
||||
|
||||
def test_init_app_registers_get_themes_global(self):
|
||||
"""Test that get_themes is registered as Jinja global."""
|
||||
from local_deep_research.web.utils.theme_helper import ThemeHelper
|
||||
from local_deep_research.web.themes import get_themes
|
||||
|
||||
helper = ThemeHelper()
|
||||
mock_app = Mock()
|
||||
mock_app.jinja_env = Mock()
|
||||
mock_app.jinja_env.globals = {}
|
||||
|
||||
helper.init_app(mock_app)
|
||||
assert "get_themes" in mock_app.jinja_env.globals
|
||||
assert mock_app.jinja_env.globals["get_themes"] == get_themes
|
||||
|
||||
def test_init_app_registers_get_themes_json_global(self):
|
||||
"""Test that get_themes_json is registered as Jinja global."""
|
||||
from local_deep_research.web.utils.theme_helper import ThemeHelper
|
||||
from local_deep_research.web.themes import get_themes_json
|
||||
|
||||
helper = ThemeHelper()
|
||||
mock_app = Mock()
|
||||
mock_app.jinja_env = Mock()
|
||||
mock_app.jinja_env.globals = {}
|
||||
|
||||
helper.init_app(mock_app)
|
||||
assert "get_themes_json" in mock_app.jinja_env.globals
|
||||
assert mock_app.jinja_env.globals["get_themes_json"] == get_themes_json
|
||||
|
||||
def test_init_app_registers_get_theme_metadata_global(self):
|
||||
"""Test that get_theme_metadata is registered as Jinja global."""
|
||||
from local_deep_research.web.utils.theme_helper import ThemeHelper
|
||||
from local_deep_research.web.themes import get_theme_metadata
|
||||
|
||||
helper = ThemeHelper()
|
||||
mock_app = Mock()
|
||||
mock_app.jinja_env = Mock()
|
||||
mock_app.jinja_env.globals = {}
|
||||
|
||||
helper.init_app(mock_app)
|
||||
assert "get_theme_metadata" in mock_app.jinja_env.globals
|
||||
assert (
|
||||
mock_app.jinja_env.globals["get_theme_metadata"]
|
||||
== get_theme_metadata
|
||||
)
|
||||
|
||||
|
||||
class TestThemeHelperGetThemes:
|
||||
"""Tests for ThemeHelper.get_themes method."""
|
||||
|
||||
def test_get_themes_returns_list(self):
|
||||
"""Test that get_themes returns a list."""
|
||||
from local_deep_research.web.utils.theme_helper import ThemeHelper
|
||||
|
||||
helper = ThemeHelper()
|
||||
themes = helper.get_themes()
|
||||
|
||||
assert isinstance(themes, list)
|
||||
|
||||
def test_get_themes_delegates_to_registry(self):
|
||||
"""Test that get_themes delegates to theme_registry."""
|
||||
from local_deep_research.web.utils.theme_helper import ThemeHelper
|
||||
|
||||
with patch(
|
||||
"local_deep_research.web.utils.theme_helper.theme_registry"
|
||||
) as mock_registry:
|
||||
mock_registry.get_theme_ids.return_value = [
|
||||
"dark",
|
||||
"light",
|
||||
"ocean",
|
||||
]
|
||||
|
||||
helper = ThemeHelper()
|
||||
themes = helper.get_themes()
|
||||
|
||||
mock_registry.get_theme_ids.assert_called_once()
|
||||
assert themes == ["dark", "light", "ocean"]
|
||||
|
||||
|
||||
class TestThemeHelperClearCache:
|
||||
"""Tests for ThemeHelper.clear_cache method."""
|
||||
|
||||
def test_clear_cache_delegates_to_registry(self):
|
||||
"""Test that clear_cache delegates to theme_registry."""
|
||||
from local_deep_research.web.utils.theme_helper import ThemeHelper
|
||||
|
||||
with patch(
|
||||
"local_deep_research.web.utils.theme_helper.theme_registry"
|
||||
) as mock_registry:
|
||||
helper = ThemeHelper()
|
||||
helper.clear_cache()
|
||||
|
||||
mock_registry.clear_cache.assert_called_once()
|
||||
|
||||
|
||||
class TestThemeHelperSingleton:
|
||||
"""Tests for the singleton theme_helper instance."""
|
||||
|
||||
def test_singleton_is_theme_helper_instance(self):
|
||||
"""Test that singleton is a ThemeHelper instance."""
|
||||
from local_deep_research.web.utils.theme_helper import (
|
||||
theme_helper,
|
||||
ThemeHelper,
|
||||
)
|
||||
|
||||
assert isinstance(theme_helper, ThemeHelper)
|
||||
@@ -0,0 +1,315 @@
|
||||
"""Tests for web/utils/vite_helper.py."""
|
||||
|
||||
import json
|
||||
from unittest.mock import Mock, patch, mock_open
|
||||
|
||||
|
||||
class TestViteHelperInit:
|
||||
"""Tests for ViteHelper initialization."""
|
||||
|
||||
def test_init_without_app(self):
|
||||
"""Test initialization without Flask app."""
|
||||
from local_deep_research.web.utils.vite_helper import ViteHelper
|
||||
|
||||
helper = ViteHelper()
|
||||
assert helper.app is None
|
||||
assert helper.manifest is None
|
||||
assert helper.is_dev is False
|
||||
|
||||
def test_init_with_app_calls_init_app(self):
|
||||
"""Test that initialization with app calls init_app."""
|
||||
from local_deep_research.web.utils.vite_helper import ViteHelper
|
||||
|
||||
mock_app = Mock()
|
||||
mock_app.debug = False
|
||||
mock_app.config = {}
|
||||
mock_app.jinja_env = Mock()
|
||||
mock_app.jinja_env.globals = {}
|
||||
|
||||
with patch.object(ViteHelper, "_load_manifest"):
|
||||
helper = ViteHelper(app=mock_app)
|
||||
assert helper.app is mock_app
|
||||
|
||||
|
||||
class TestViteHelperInitApp:
|
||||
"""Tests for ViteHelper.init_app method."""
|
||||
|
||||
def test_init_app_sets_app_attribute(self):
|
||||
"""Test that init_app sets the app attribute."""
|
||||
from local_deep_research.web.utils.vite_helper import ViteHelper
|
||||
|
||||
helper = ViteHelper()
|
||||
mock_app = Mock()
|
||||
mock_app.debug = False
|
||||
mock_app.config = {}
|
||||
mock_app.jinja_env = Mock()
|
||||
mock_app.jinja_env.globals = {}
|
||||
|
||||
with patch.object(helper, "_load_manifest"):
|
||||
helper.init_app(mock_app)
|
||||
assert helper.app is mock_app
|
||||
|
||||
def test_init_app_sets_dev_mode_from_debug(self):
|
||||
"""Test that is_dev is set from app.debug."""
|
||||
from local_deep_research.web.utils.vite_helper import ViteHelper
|
||||
|
||||
helper = ViteHelper()
|
||||
mock_app = Mock()
|
||||
mock_app.debug = True
|
||||
mock_app.config = {}
|
||||
mock_app.jinja_env = Mock()
|
||||
mock_app.jinja_env.globals = {}
|
||||
|
||||
helper.init_app(mock_app)
|
||||
assert helper.is_dev is True
|
||||
|
||||
def test_init_app_sets_dev_mode_from_config(self):
|
||||
"""Test that is_dev can be set from VITE_DEV_MODE config."""
|
||||
from local_deep_research.web.utils.vite_helper import ViteHelper
|
||||
|
||||
helper = ViteHelper()
|
||||
mock_app = Mock()
|
||||
mock_app.debug = False
|
||||
mock_app.config = {"VITE_DEV_MODE": True}
|
||||
mock_app.jinja_env = Mock()
|
||||
mock_app.jinja_env.globals = {}
|
||||
|
||||
helper.init_app(mock_app)
|
||||
assert helper.is_dev is True
|
||||
|
||||
def test_init_app_registers_vite_asset_global(self):
|
||||
"""Test that vite_asset is registered as Jinja global."""
|
||||
from local_deep_research.web.utils.vite_helper import ViteHelper
|
||||
|
||||
helper = ViteHelper()
|
||||
mock_app = Mock()
|
||||
mock_app.debug = True
|
||||
mock_app.config = {}
|
||||
mock_app.jinja_env = Mock()
|
||||
mock_app.jinja_env.globals = {}
|
||||
|
||||
helper.init_app(mock_app)
|
||||
assert "vite_asset" in mock_app.jinja_env.globals
|
||||
assert mock_app.jinja_env.globals["vite_asset"] == helper.vite_asset
|
||||
|
||||
def test_init_app_registers_vite_hmr_global(self):
|
||||
"""Test that vite_hmr is registered as Jinja global."""
|
||||
from local_deep_research.web.utils.vite_helper import ViteHelper
|
||||
|
||||
helper = ViteHelper()
|
||||
mock_app = Mock()
|
||||
mock_app.debug = True
|
||||
mock_app.config = {}
|
||||
mock_app.jinja_env = Mock()
|
||||
mock_app.jinja_env.globals = {}
|
||||
|
||||
helper.init_app(mock_app)
|
||||
assert "vite_hmr" in mock_app.jinja_env.globals
|
||||
assert mock_app.jinja_env.globals["vite_hmr"] == helper.vite_hmr
|
||||
|
||||
def test_init_app_loads_manifest_in_production(self):
|
||||
"""Test that manifest is loaded in production mode."""
|
||||
from local_deep_research.web.utils.vite_helper import ViteHelper
|
||||
|
||||
helper = ViteHelper()
|
||||
mock_app = Mock()
|
||||
mock_app.debug = False
|
||||
mock_app.config = {}
|
||||
mock_app.jinja_env = Mock()
|
||||
mock_app.jinja_env.globals = {}
|
||||
|
||||
with patch.object(helper, "_load_manifest") as mock_load:
|
||||
helper.init_app(mock_app)
|
||||
mock_load.assert_called_once()
|
||||
|
||||
def test_init_app_skips_manifest_in_dev(self):
|
||||
"""Test that manifest loading is skipped in dev mode."""
|
||||
from local_deep_research.web.utils.vite_helper import ViteHelper
|
||||
|
||||
helper = ViteHelper()
|
||||
mock_app = Mock()
|
||||
mock_app.debug = True
|
||||
mock_app.config = {}
|
||||
mock_app.jinja_env = Mock()
|
||||
mock_app.jinja_env.globals = {}
|
||||
|
||||
with patch.object(helper, "_load_manifest") as mock_load:
|
||||
helper.init_app(mock_app)
|
||||
mock_load.assert_not_called()
|
||||
|
||||
|
||||
class TestViteHelperLoadManifest:
|
||||
"""Tests for ViteHelper._load_manifest method."""
|
||||
|
||||
def test_load_manifest_reads_json_file(self):
|
||||
"""Test that manifest JSON is loaded from file."""
|
||||
from local_deep_research.web.utils.vite_helper import ViteHelper
|
||||
|
||||
helper = ViteHelper()
|
||||
helper.app = Mock()
|
||||
helper.app.config = {"STATIC_DIR": "/app/static"}
|
||||
|
||||
manifest_data = {"js/app.js": {"file": "assets/app-abc123.js"}}
|
||||
|
||||
with patch("pathlib.Path.exists", return_value=True):
|
||||
with patch(
|
||||
"builtins.open", mock_open(read_data=json.dumps(manifest_data))
|
||||
):
|
||||
helper._load_manifest()
|
||||
assert helper.manifest == manifest_data
|
||||
|
||||
def test_load_manifest_uses_default_static_dir(self):
|
||||
"""Test that default static directory is used when not configured."""
|
||||
from local_deep_research.web.utils.vite_helper import ViteHelper
|
||||
|
||||
helper = ViteHelper()
|
||||
helper.app = Mock()
|
||||
helper.app.config = {}
|
||||
|
||||
with patch("pathlib.Path.exists", return_value=False):
|
||||
helper._load_manifest()
|
||||
assert helper.manifest == {}
|
||||
|
||||
def test_load_manifest_handles_missing_file(self):
|
||||
"""Test that missing manifest file results in empty dict."""
|
||||
from local_deep_research.web.utils.vite_helper import ViteHelper
|
||||
|
||||
helper = ViteHelper()
|
||||
helper.app = Mock()
|
||||
helper.app.config = {"STATIC_DIR": "/app/static"}
|
||||
|
||||
with patch("pathlib.Path.exists", return_value=False):
|
||||
helper._load_manifest()
|
||||
assert helper.manifest == {}
|
||||
|
||||
|
||||
class TestViteHelperViteHmr:
|
||||
"""Tests for ViteHelper.vite_hmr method."""
|
||||
|
||||
def test_vite_hmr_returns_script_in_dev_mode(self):
|
||||
"""Test that HMR script is returned in development mode."""
|
||||
from local_deep_research.web.utils.vite_helper import ViteHelper
|
||||
|
||||
helper = ViteHelper()
|
||||
helper.is_dev = True
|
||||
|
||||
result = helper.vite_hmr()
|
||||
assert "localhost:5173" in str(result)
|
||||
assert "@vite/client" in str(result)
|
||||
assert '<script type="module"' in str(result)
|
||||
|
||||
def test_vite_hmr_returns_empty_in_production(self):
|
||||
"""Test that empty string is returned in production mode."""
|
||||
from local_deep_research.web.utils.vite_helper import ViteHelper
|
||||
|
||||
helper = ViteHelper()
|
||||
helper.is_dev = False
|
||||
|
||||
result = helper.vite_hmr()
|
||||
assert result == ""
|
||||
|
||||
|
||||
class TestViteHelperViteAsset:
|
||||
"""Tests for ViteHelper.vite_asset method."""
|
||||
|
||||
def test_vite_asset_returns_dev_server_url_in_dev_mode(self):
|
||||
"""Test that dev server URL is returned in development mode."""
|
||||
from local_deep_research.web.utils.vite_helper import ViteHelper
|
||||
|
||||
helper = ViteHelper()
|
||||
helper.is_dev = True
|
||||
|
||||
result = helper.vite_asset("js/app.js")
|
||||
assert "localhost:5173" in str(result)
|
||||
assert "js/app.js" in str(result)
|
||||
assert '<script type="module"' in str(result)
|
||||
|
||||
def test_vite_asset_uses_default_entry_point(self):
|
||||
"""Test that default entry point is used when not specified."""
|
||||
from local_deep_research.web.utils.vite_helper import ViteHelper
|
||||
|
||||
helper = ViteHelper()
|
||||
helper.is_dev = True
|
||||
|
||||
result = helper.vite_asset()
|
||||
assert "js/app.js" in str(result)
|
||||
|
||||
def test_vite_asset_returns_manifest_path_in_production(self):
|
||||
"""Test that manifest file path is returned in production."""
|
||||
from local_deep_research.web.utils.vite_helper import ViteHelper
|
||||
|
||||
helper = ViteHelper()
|
||||
helper.is_dev = False
|
||||
helper.manifest = {
|
||||
"js/app.js": {
|
||||
"file": "assets/app-abc123.js",
|
||||
}
|
||||
}
|
||||
|
||||
result = helper.vite_asset("js/app.js")
|
||||
assert "/static/dist/assets/app-abc123.js" in str(result)
|
||||
assert '<script type="module"' in str(result)
|
||||
|
||||
def test_vite_asset_includes_css_from_manifest(self):
|
||||
"""Test that CSS files from manifest are included."""
|
||||
from local_deep_research.web.utils.vite_helper import ViteHelper
|
||||
|
||||
helper = ViteHelper()
|
||||
helper.is_dev = False
|
||||
helper.manifest = {
|
||||
"js/app.js": {
|
||||
"file": "assets/app-abc123.js",
|
||||
"css": ["assets/app-abc123.css", "assets/vendor-def456.css"],
|
||||
}
|
||||
}
|
||||
|
||||
result = helper.vite_asset("js/app.js")
|
||||
assert "/static/dist/assets/app-abc123.css" in str(result)
|
||||
assert "/static/dist/assets/vendor-def456.css" in str(result)
|
||||
assert '<link rel="stylesheet"' in str(result)
|
||||
|
||||
def test_vite_asset_returns_fallback_when_no_manifest(self):
|
||||
"""Test that fallback is returned when manifest is empty."""
|
||||
from local_deep_research.web.utils.vite_helper import ViteHelper
|
||||
|
||||
helper = ViteHelper()
|
||||
helper.is_dev = False
|
||||
helper.manifest = {}
|
||||
|
||||
result = helper.vite_asset("js/app.js")
|
||||
assert "Vite build not found" in str(result)
|
||||
|
||||
def test_vite_asset_returns_fallback_for_missing_entry(self):
|
||||
"""Test that fallback is returned for missing entry point."""
|
||||
from local_deep_research.web.utils.vite_helper import ViteHelper
|
||||
|
||||
helper = ViteHelper()
|
||||
helper.is_dev = False
|
||||
helper.manifest = {"other/file.js": {"file": "assets/other.js"}}
|
||||
|
||||
result = helper.vite_asset("js/missing.js")
|
||||
assert "Vite build not found" in str(result)
|
||||
|
||||
|
||||
class TestViteHelperFallbackAssets:
|
||||
"""Tests for ViteHelper._fallback_assets method."""
|
||||
|
||||
def test_fallback_assets_returns_comment(self):
|
||||
"""Test that fallback returns informative HTML comment."""
|
||||
from local_deep_research.web.utils.vite_helper import ViteHelper
|
||||
|
||||
helper = ViteHelper()
|
||||
result = helper._fallback_assets()
|
||||
|
||||
assert "Vite build not found" in str(result)
|
||||
assert "npm run build" in str(result)
|
||||
|
||||
|
||||
class TestViteSingleton:
|
||||
"""Tests for the singleton vite instance."""
|
||||
|
||||
def test_singleton_is_vite_helper_instance(self):
|
||||
"""Test that singleton is a ViteHelper instance."""
|
||||
from local_deep_research.web.utils.vite_helper import vite, ViteHelper
|
||||
|
||||
assert isinstance(vite, ViteHelper)
|
||||
Reference in New Issue
Block a user