7a0da7932b
Backwards Compatibility / Verify Encryption Constants (push) Waiting to run
Backwards Compatibility / PyPI Version Compatibility (push) Waiting to run
Backwards Compatibility / Database Migration Tests (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
CodeQL Advanced / Analyze (python) (push) Waiting to run
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Blocked by required conditions
Docker Tests (Consolidated) / detect-changes (push) Waiting to run
Docker Tests (Consolidated) / Build Test Image (push) Waiting to run
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Blocked by required conditions
Docker Tests (Consolidated) / Accessibility Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Unit Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Example Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / Production Image Smoke Test (push) Blocked by required conditions
Docker Tests (Consolidated) / Infrastructure Tests (push) Blocked by required conditions
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Waiting to run
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
340 lines
12 KiB
Python
340 lines
12 KiB
Python
"""
|
|
Behavioral tests for web/exceptions module.
|
|
|
|
Tests custom exception classes, status codes, error codes, and JSON serialization.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
|
|
class TestWebAPIException:
|
|
"""Tests for WebAPIException class."""
|
|
|
|
def test_is_exception_subclass(self):
|
|
"""WebAPIException is an Exception subclass."""
|
|
from local_deep_research.web.exceptions import WebAPIException
|
|
|
|
assert issubclass(WebAPIException, Exception)
|
|
|
|
def test_default_status_code_is_500(self):
|
|
"""Default status code is 500."""
|
|
from local_deep_research.web.exceptions import WebAPIException
|
|
|
|
exc = WebAPIException("test error")
|
|
assert exc.status_code == 500
|
|
|
|
def test_custom_status_code(self):
|
|
"""Accepts custom status code."""
|
|
from local_deep_research.web.exceptions import WebAPIException
|
|
|
|
exc = WebAPIException("not found", status_code=404)
|
|
assert exc.status_code == 404
|
|
|
|
def test_message_stored(self):
|
|
"""Message is stored as attribute."""
|
|
from local_deep_research.web.exceptions import WebAPIException
|
|
|
|
exc = WebAPIException("something went wrong")
|
|
assert exc.message == "something went wrong"
|
|
|
|
def test_message_in_str(self):
|
|
"""Message appears in str representation."""
|
|
from local_deep_research.web.exceptions import WebAPIException
|
|
|
|
exc = WebAPIException("something went wrong")
|
|
assert str(exc) == "something went wrong"
|
|
|
|
def test_default_error_code_is_class_name(self):
|
|
"""Default error code is the class name."""
|
|
from local_deep_research.web.exceptions import WebAPIException
|
|
|
|
exc = WebAPIException("test")
|
|
assert exc.error_code == "WebAPIException"
|
|
|
|
def test_custom_error_code(self):
|
|
"""Accepts custom error code."""
|
|
from local_deep_research.web.exceptions import WebAPIException
|
|
|
|
exc = WebAPIException("test", error_code="CUSTOM_ERROR")
|
|
assert exc.error_code == "CUSTOM_ERROR"
|
|
|
|
def test_default_details_is_empty_dict(self):
|
|
"""Default details is empty dict."""
|
|
from local_deep_research.web.exceptions import WebAPIException
|
|
|
|
exc = WebAPIException("test")
|
|
assert exc.details == {}
|
|
|
|
def test_custom_details(self):
|
|
"""Accepts custom details dict."""
|
|
from local_deep_research.web.exceptions import WebAPIException
|
|
|
|
details = {"field": "email", "reason": "invalid"}
|
|
exc = WebAPIException("test", details=details)
|
|
assert exc.details == details
|
|
|
|
def test_can_be_raised_and_caught(self):
|
|
"""Can be raised and caught."""
|
|
from local_deep_research.web.exceptions import WebAPIException
|
|
|
|
with pytest.raises(WebAPIException):
|
|
raise WebAPIException("test")
|
|
|
|
|
|
class TestWebAPIExceptionToDict:
|
|
"""Tests for WebAPIException.to_dict method."""
|
|
|
|
def test_to_dict_has_status_key(self):
|
|
"""to_dict includes 'status' key."""
|
|
from local_deep_research.web.exceptions import WebAPIException
|
|
|
|
exc = WebAPIException("test")
|
|
result = exc.to_dict()
|
|
assert result["status"] == "error"
|
|
|
|
def test_to_dict_has_message(self):
|
|
"""to_dict includes message."""
|
|
from local_deep_research.web.exceptions import WebAPIException
|
|
|
|
exc = WebAPIException("something failed")
|
|
result = exc.to_dict()
|
|
assert result["message"] == "something failed"
|
|
|
|
def test_to_dict_has_error_code(self):
|
|
"""to_dict includes error_code."""
|
|
from local_deep_research.web.exceptions import WebAPIException
|
|
|
|
exc = WebAPIException("test", error_code="MY_ERROR")
|
|
result = exc.to_dict()
|
|
assert result["error_code"] == "MY_ERROR"
|
|
|
|
def test_to_dict_excludes_empty_details(self):
|
|
"""to_dict does not include details when empty."""
|
|
from local_deep_research.web.exceptions import WebAPIException
|
|
|
|
exc = WebAPIException("test")
|
|
result = exc.to_dict()
|
|
assert "details" not in result
|
|
|
|
def test_to_dict_includes_details_when_present(self):
|
|
"""to_dict includes details when non-empty."""
|
|
from local_deep_research.web.exceptions import WebAPIException
|
|
|
|
exc = WebAPIException("test", details={"key": "value"})
|
|
result = exc.to_dict()
|
|
assert result["details"] == {"key": "value"}
|
|
|
|
def test_to_dict_returns_dict(self):
|
|
"""to_dict returns a dictionary."""
|
|
from local_deep_research.web.exceptions import WebAPIException
|
|
|
|
exc = WebAPIException("test")
|
|
assert isinstance(exc.to_dict(), dict)
|
|
|
|
|
|
class TestAuthenticationRequiredError:
|
|
"""Tests for AuthenticationRequiredError class."""
|
|
|
|
def test_is_web_api_exception_subclass(self):
|
|
"""AuthenticationRequiredError is a WebAPIException subclass."""
|
|
from local_deep_research.web.exceptions import (
|
|
AuthenticationRequiredError,
|
|
WebAPIException,
|
|
)
|
|
|
|
assert issubclass(AuthenticationRequiredError, WebAPIException)
|
|
|
|
def test_default_status_code_is_401(self):
|
|
"""Default status code is 401."""
|
|
from local_deep_research.web.exceptions import (
|
|
AuthenticationRequiredError,
|
|
)
|
|
|
|
exc = AuthenticationRequiredError()
|
|
assert exc.status_code == 401
|
|
|
|
def test_default_message(self):
|
|
"""Has a default message about authentication."""
|
|
from local_deep_research.web.exceptions import (
|
|
AuthenticationRequiredError,
|
|
)
|
|
|
|
exc = AuthenticationRequiredError()
|
|
assert "Authentication required" in exc.message
|
|
|
|
def test_custom_message(self):
|
|
"""Accepts custom message."""
|
|
from local_deep_research.web.exceptions import (
|
|
AuthenticationRequiredError,
|
|
)
|
|
|
|
exc = AuthenticationRequiredError(message="Login expired")
|
|
assert exc.message == "Login expired"
|
|
|
|
def test_error_code_is_authentication_required(self):
|
|
"""Error code is AUTHENTICATION_REQUIRED."""
|
|
from local_deep_research.web.exceptions import (
|
|
AuthenticationRequiredError,
|
|
)
|
|
|
|
exc = AuthenticationRequiredError()
|
|
assert exc.error_code == "AUTHENTICATION_REQUIRED"
|
|
|
|
def test_no_username_means_empty_details(self):
|
|
"""No username means empty details."""
|
|
from local_deep_research.web.exceptions import (
|
|
AuthenticationRequiredError,
|
|
)
|
|
|
|
exc = AuthenticationRequiredError()
|
|
assert exc.details == {}
|
|
|
|
def test_username_stored_in_details(self):
|
|
"""Username is stored in details dict."""
|
|
from local_deep_research.web.exceptions import (
|
|
AuthenticationRequiredError,
|
|
)
|
|
|
|
exc = AuthenticationRequiredError(username="admin")
|
|
assert exc.details["username"] == "admin"
|
|
|
|
def test_to_dict_includes_username(self):
|
|
"""to_dict includes username in details."""
|
|
from local_deep_research.web.exceptions import (
|
|
AuthenticationRequiredError,
|
|
)
|
|
|
|
exc = AuthenticationRequiredError(username="testuser")
|
|
result = exc.to_dict()
|
|
assert result["details"]["username"] == "testuser"
|
|
|
|
def test_can_be_caught_as_web_api_exception(self):
|
|
"""Can be caught as WebAPIException."""
|
|
from local_deep_research.web.exceptions import (
|
|
AuthenticationRequiredError,
|
|
WebAPIException,
|
|
)
|
|
|
|
with pytest.raises(WebAPIException):
|
|
raise AuthenticationRequiredError()
|
|
|
|
|
|
class TestWebToDictSanitization:
|
|
"""to_dict() runs message + details string leaves through the central
|
|
sanitizers before the two WebAPIException handlers jsonify them.
|
|
|
|
Mirrors the NewsAPIException backstop: credential-shape redaction that
|
|
leaves legitimate content (the class a denylist would false-positive on)
|
|
untouched.
|
|
"""
|
|
|
|
def test_benign_message_passes_through_unchanged(self):
|
|
from local_deep_research.web.exceptions import WebAPIException
|
|
|
|
msg = (
|
|
"Authentication required: Please refresh the page and log in again."
|
|
)
|
|
assert WebAPIException(msg).to_dict()["message"] == msg
|
|
|
|
def test_credential_keyword_in_prose_not_scrubbed(self):
|
|
from local_deep_research.web.exceptions import WebAPIException
|
|
|
|
msg = "how do I reset my password?"
|
|
assert WebAPIException(msg).to_dict()["message"] == msg
|
|
|
|
def test_credential_in_message_redacted(self):
|
|
from local_deep_research.web.exceptions import WebAPIException
|
|
|
|
exc = WebAPIException(
|
|
"auth failed: Authorization: Bearer sk-abc123DEF456ghi789"
|
|
)
|
|
message = exc.to_dict()["message"]
|
|
assert "sk-abc123DEF456ghi789" not in message
|
|
assert "REDACTED" in message
|
|
|
|
def test_long_message_truncated_to_client_cap(self):
|
|
from local_deep_research.web.exceptions import WebAPIException
|
|
|
|
message = WebAPIException("A" * 300).to_dict()["message"]
|
|
assert len(message) <= 200
|
|
assert message.endswith("...")
|
|
|
|
def test_stored_message_attribute_is_not_mutated(self):
|
|
from local_deep_research.web.exceptions import WebAPIException
|
|
|
|
raw = "boom: Authorization: Bearer sk-secret999value000"
|
|
exc = WebAPIException(raw)
|
|
assert exc.to_dict()["message"] != raw
|
|
assert exc.message == raw
|
|
|
|
def test_status_and_error_code_untouched(self):
|
|
from local_deep_research.web.exceptions import WebAPIException
|
|
|
|
d = WebAPIException(
|
|
"Bearer sk-abc123DEF456ghi789",
|
|
status_code=502,
|
|
error_code="UPSTREAM",
|
|
).to_dict()
|
|
assert d["status"] == "error"
|
|
assert d["error_code"] == "UPSTREAM"
|
|
|
|
def test_benign_details_pass_through_unchanged(self):
|
|
from local_deep_research.web.exceptions import WebAPIException
|
|
|
|
exc = WebAPIException(
|
|
"err", details={"username": "testuser", "attempts": 3}
|
|
)
|
|
assert exc.to_dict()["details"] == {
|
|
"username": "testuser",
|
|
"attempts": 3,
|
|
}
|
|
|
|
def test_credential_in_details_string_leaf_redacted(self):
|
|
from local_deep_research.web.exceptions import WebAPIException
|
|
|
|
exc = WebAPIException(
|
|
"err",
|
|
details={"note": "Authorization: Bearer sk-abc123DEF456ghi789"},
|
|
)
|
|
note = exc.to_dict()["details"]["note"]
|
|
assert "sk-abc123DEF456ghi789" not in note
|
|
assert "REDACTED" in note
|
|
|
|
def test_namedtuple_detail_does_not_crash(self):
|
|
# Regression guard: a namedtuple in details must not crash to_dict()
|
|
# inside the error handler (the shared helper rebuilds it as a list).
|
|
from collections import namedtuple
|
|
|
|
from local_deep_research.web.exceptions import WebAPIException
|
|
|
|
Pair = namedtuple("Pair", ["a", "b"])
|
|
d = WebAPIException(
|
|
"err", details={"pair": Pair("Bearer sk-abc123DEF456ghi789", "x")}
|
|
).to_dict()["details"]["pair"]
|
|
assert isinstance(d, list)
|
|
assert "sk-abc123DEF456ghi789" not in d[0]
|
|
assert "REDACTED" in d[0]
|
|
assert d[1] == "x"
|
|
|
|
def test_stored_details_attribute_is_not_mutated(self):
|
|
from local_deep_research.web.exceptions import WebAPIException
|
|
|
|
raw = {"note": "Bearer sk-secret999value000"}
|
|
exc = WebAPIException("err", details=raw)
|
|
assert "REDACTED" in exc.to_dict()["details"]["note"]
|
|
assert exc.details["note"] == "Bearer sk-secret999value000"
|
|
|
|
def test_subclass_details_sanitized_via_inherited_to_dict(self):
|
|
# The PR's rationale: a subclass that forwards caller data into details
|
|
# still gets the backstop, because to_dict() is inherited.
|
|
from local_deep_research.web.exceptions import (
|
|
AuthenticationRequiredError,
|
|
)
|
|
|
|
exc = AuthenticationRequiredError(
|
|
username="Bearer sk-abc123DEF456ghi789"
|
|
)
|
|
username = exc.to_dict()["details"]["username"]
|
|
assert "sk-abc123DEF456ghi789" not in username
|
|
assert "REDACTED" in username
|