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

244 lines
9.3 KiB
Python

"""Tests for _check_response_size() and SafeSession.send() intermediate redirect branching.
Covers:
- _check_response_size() direct unit tests (8 tests) — zero prior coverage
- SafeSession.send() intermediate redirect size-check logic (10 tests) — lines 437-443
"""
import pytest
from unittest.mock import patch, MagicMock
import requests
from local_deep_research.security import ssrf_validator
from local_deep_research.security.safe_requests import (
_check_response_size,
SafeSession,
MAX_RESPONSE_SIZE,
_REDIRECT_STATUS_CODES,
)
def _mock_response(status_code=200, headers=None):
"""Build a MagicMock Response with the given status and headers."""
resp = MagicMock(spec=requests.Response)
resp.status_code = status_code
resp.headers = headers or {}
resp.url = "https://example.com"
# _check_response_size may install a body guard via _install_body_guard,
# which accesses response.raw.read
resp.raw = MagicMock()
return resp
# ── _check_response_size() direct unit tests ────────────────────────
class TestCheckResponseSizeDirect:
"""Direct unit tests for _check_response_size (lines 29-53)."""
def test_no_content_length_header(self):
"""No Content-Length header → no raise."""
resp = _mock_response(headers={})
_check_response_size(resp) # should not raise
def test_empty_string_content_length(self):
"""Empty string Content-Length is falsy → no raise."""
resp = _mock_response(headers={"Content-Length": ""})
_check_response_size(resp)
def test_negative_content_length(self):
"""Negative Content-Length → returns silently (line 46-47)."""
resp = _mock_response(headers={"Content-Length": "-1"})
_check_response_size(resp)
def test_non_numeric_content_length(self):
"""Non-numeric Content-Length → ValueError caught, returns (line 44)."""
resp = _mock_response(headers={"Content-Length": "abc"})
_check_response_size(resp)
def test_content_length_type_error(self):
"""Content-Length value that causes TypeError in int() → caught (line 44)."""
resp = _mock_response(headers={"Content-Length": None})
_check_response_size(resp)
def test_exactly_at_max_size(self):
"""Content-Length exactly at MAX_RESPONSE_SIZE → no raise (> not >=)."""
resp = _mock_response(
headers={"Content-Length": str(MAX_RESPONSE_SIZE)}
)
_check_response_size(resp)
def test_one_over_max_raises(self):
"""Content-Length one over MAX → raises ValueError, response.close() called."""
resp = _mock_response(
headers={"Content-Length": str(MAX_RESPONSE_SIZE + 1)}
)
with pytest.raises(ValueError, match="Response too large"):
_check_response_size(resp)
resp.close.assert_called_once()
def test_normal_size_under_limit(self):
"""Normal Content-Length well under limit → no raise."""
resp = _mock_response(headers={"Content-Length": "1024"})
_check_response_size(resp)
# ── SafeSession.send() intermediate redirect branching ──────────────
def _prep_request(url="https://example.com"):
"""Build a minimal PreparedRequest."""
prep = requests.PreparedRequest()
prep.prepare_url(url, {})
prep.prepare_method("GET")
return prep
@pytest.fixture
def _bypass_ssrf():
"""Patch validate_url to always return True (bypass SSRF check)."""
with patch.object(
ssrf_validator,
"validate_url",
return_value=True,
):
yield
class TestSendIntermediateRedirectBranching:
"""Tests for is_intermediate_redirect logic (lines 437-443)."""
def test_302_with_location_and_allow_redirects_applies_size_check(
self, _bypass_ssrf
):
"""302 + Location + allow_redirects=True → size check still applies."""
oversized = _mock_response(
302,
{
"Location": "https://other.com",
"Content-Length": str(MAX_RESPONSE_SIZE + 1),
},
)
session = SafeSession()
with patch.object(requests.Session, "send", return_value=oversized):
with pytest.raises(ValueError, match="Response too large"):
session.send(_prep_request(), allow_redirects=True)
def test_302_with_location_and_allow_redirects_false_applies_size_check(
self, _bypass_ssrf
):
"""302 + Location + allow_redirects=False → apply size check → raises."""
oversized = _mock_response(
302,
{
"Location": "https://other.com",
"Content-Length": str(MAX_RESPONSE_SIZE + 1),
},
)
session = SafeSession()
with patch.object(requests.Session, "send", return_value=oversized):
with pytest.raises(ValueError, match="Response too large"):
session.send(_prep_request(), allow_redirects=False)
def test_302_without_location_applies_size_check(self, _bypass_ssrf):
"""302 without Location header → apply size check → raises."""
oversized = _mock_response(
302, {"Content-Length": str(MAX_RESPONSE_SIZE + 1)}
)
session = SafeSession()
with patch.object(requests.Session, "send", return_value=oversized):
with pytest.raises(ValueError, match="Response too large"):
session.send(_prep_request(), allow_redirects=True)
def test_302_without_location_normal_size_passes(self, _bypass_ssrf):
"""302 without Location, normal Content-Length → no raise."""
resp = _mock_response(302, {"Content-Length": "512"})
session = SafeSession()
with patch.object(requests.Session, "send", return_value=resp):
result = session.send(_prep_request(), allow_redirects=True)
assert result.status_code == 302
def test_200_normal_size_passes(self, _bypass_ssrf):
"""200 response, normal size → passes."""
resp = _mock_response(200, {"Content-Length": "1024"})
session = SafeSession()
with patch.object(requests.Session, "send", return_value=resp):
result = session.send(_prep_request())
assert result.status_code == 200
def test_200_oversized_raises(self, _bypass_ssrf):
"""200 response, oversized Content-Length → raises."""
oversized = _mock_response(
200, {"Content-Length": str(MAX_RESPONSE_SIZE + 1)}
)
session = SafeSession()
with patch.object(requests.Session, "send", return_value=oversized):
with pytest.raises(ValueError, match="Response too large"):
session.send(_prep_request())
@pytest.mark.parametrize("status_code", [301, 302, 303, 307, 308])
def test_all_redirect_codes_apply_size_check(
self, _bypass_ssrf, status_code
):
"""All 5 redirect codes + Location + oversized → rejected by size check."""
oversized = _mock_response(
status_code,
{
"Location": "https://other.com",
"Content-Length": str(MAX_RESPONSE_SIZE + 1),
},
)
session = SafeSession()
with patch.object(requests.Session, "send", return_value=oversized):
with pytest.raises(ValueError, match="Response too large"):
session.send(_prep_request(), allow_redirects=True)
def test_allow_redirects_defaults_to_true_still_checks_size(
self, _bypass_ssrf
):
"""No allow_redirects in kwargs → size check still applies."""
oversized = _mock_response(
302,
{
"Location": "https://other.com",
"Content-Length": str(MAX_RESPONSE_SIZE + 1),
},
)
session = SafeSession()
with patch.object(requests.Session, "send", return_value=oversized):
with pytest.raises(ValueError, match="Response too large"):
session.send(_prep_request())
def test_304_not_in_redirect_set_applies_size_check(self, _bypass_ssrf):
"""304 Not Modified is NOT in the redirect frozenset → size check applies."""
assert 304 not in _REDIRECT_STATUS_CODES
oversized = _mock_response(
304,
{
"Location": "https://other.com",
"Content-Length": str(MAX_RESPONSE_SIZE + 1),
},
)
session = SafeSession()
with patch.object(requests.Session, "send", return_value=oversized):
with pytest.raises(ValueError, match="Response too large"):
session.send(_prep_request(), allow_redirects=True)
def test_oversized_intermediate_redirect_rejected(self, _bypass_ssrf):
"""301 + Location + allow_redirects=True + oversized → raises.
All responses are size-checked uniformly, including redirects.
"""
oversized = _mock_response(
301,
{
"Location": "https://other.com",
"Content-Length": str(MAX_RESPONSE_SIZE + 1),
},
)
session = SafeSession()
with patch.object(requests.Session, "send", return_value=oversized):
with pytest.raises(ValueError, match="Response too large"):
session.send(_prep_request(), allow_redirects=True)