""" 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