blob: 9101f7eefcf8ea653950497e151a782185ffbc6e [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import unittest
from unittest.mock import Mock
import pytest
import requests
from pyhugegraph.utils.exceptions import NotAuthorizedError, NotFoundError, ResponseParseError, ServerError
from pyhugegraph.utils.util import ResponseValidation, check_if_success, redact_sensitive_data
pytestmark = pytest.mark.contract
class TestResponseValidation(unittest.TestCase):
def _mock_error_response(self, body, text):
response = Mock(spec=requests.Response)
response.status_code = 400
response.text = text
response.content = response.text.encode("utf-8")
response.json.return_value = body
response.request = Mock()
response.request.body = "g.V2()"
response.raise_for_status.side_effect = requests.exceptions.HTTPError("400 Client Error")
return response
def test_numeric_status_body_raises_server_exception_with_message(self):
response = self._mock_error_response(
{"status": 400, "message": "bad gremlin"},
'{"status":400,"message":"bad gremlin"}',
)
validator = ResponseValidation()
with self.assertRaisesRegex(Exception, "Server Exception: bad gremlin"):
validator(response, "POST", "/gremlin")
def test_non_dict_json_body_raises_server_exception_with_response_text(self):
response = self._mock_error_response(["bad gremlin"], "bad gremlin")
validator = ResponseValidation()
with self.assertRaisesRegex(Exception, "Server Exception: bad gremlin"):
validator(response, "POST", "/gremlin")
def test_backend_error_envelope_preserves_message(self):
response = Mock(spec=requests.Response)
response.status_code = 500
response.text = '{"exception":"BackendException","message":"quality failure"}'
response.content = response.text.encode("utf-8")
response.json.return_value = {"exception": "BackendException", "message": "quality failure"}
response.request = Mock(body='{"gremlin":"g.V2()"}', url="http://127.0.0.1:8080/gremlin")
response.raise_for_status.side_effect = requests.exceptions.HTTPError("500 Server Error")
validator = ResponseValidation()
with pytest.raises(Exception) as exc_info:
validator(response, method="POST", path="/gremlin")
assert "quality failure" in str(exc_info.value)
def test_malformed_error_body_uses_response_text(self):
response = self._mock_error_response(ValueError("not json"), "not json")
response.json.side_effect = ValueError("not json")
validator = ResponseValidation()
with self.assertRaisesRegex(Exception, "Server Exception: not json"):
validator(response, "POST", "/gremlin")
def test_unauthorized_error_preserves_not_authorized_type(self):
response = Mock(spec=requests.Response)
response.status_code = 401
response.text = '{"exception":"NotAuthorizedException","message":"Authentication failed"}'
response.content = response.text.encode("utf-8")
response.request = Mock(body="Empty body", url="http://127.0.0.1:8080/graphs")
response.raise_for_status.side_effect = requests.exceptions.HTTPError("401 Client Error")
validator = ResponseValidation()
with pytest.raises(NotAuthorizedError) as exc_info:
validator(response, method="GET", path="/graphs")
assert "Please check your username and password" in str(exc_info.value)
def test_malformed_success_json_raises_parse_error(self):
response = Mock(spec=requests.Response)
response.status_code = 200
response.text = "not json"
response.content = b"not json"
response.json.side_effect = ValueError("not json")
response.raise_for_status.return_value = None
validator = ResponseValidation()
with pytest.raises(ResponseParseError, match="Failed to parse json response"):
validator(response, "GET", "/graphs/hugegraph")
def test_unknown_content_type_raises_value_error_without_wrapping(self):
response = Mock(spec=requests.Response)
validator = ResponseValidation(content_type="xml")
with pytest.raises(ValueError, match="Unknown content type: xml"):
validator(response, "GET", "/graphs/hugegraph")
def test_strict_404_preserves_not_found_type(self):
response = self._mock_error_response(
{"message": "missing vertex"},
'{"message":"missing vertex"}',
)
response.status_code = 404
validator = ResponseValidation()
with pytest.raises(NotFoundError):
validator(response, "GET", "/graphs/hugegraph")
def test_error_log_preserves_plain_utf8_request_body(self):
response = self._mock_error_response(
{"message": "bad request"},
'{"message":"bad request"}',
)
response.request.body = "中文 payload"
validator = ResponseValidation()
with pytest.raises(ServerError), unittest.mock.patch("pyhugegraph.utils.util.log.error") as log_error:
validator(response, "POST", "/gremlin")
assert "中文 payload" in str(log_error.call_args)
def test_error_log_redacts_sensitive_request_body(self):
response = self._mock_error_response(
{"message": "bad request"},
'{"message":"bad request"}',
)
response.request.body = '{"user_name":"marko","user_password":"super-secret"}'
validator = ResponseValidation()
with pytest.raises(ServerError), unittest.mock.patch("pyhugegraph.utils.util.log.error") as log_error:
validator(response, "POST", "/auth/users")
logged_args = str(log_error.call_args)
assert "super-secret" not in logged_args
assert "***REDACTED***" in logged_args
def test_check_if_success_redacts_sensitive_response_body(self):
response = self._mock_error_response(
{"message": "bad request"},
'{"message":"token=server-secret"}',
)
response.request.body = '{"query":"g.V()"}'
with pytest.raises(NotFoundError), unittest.mock.patch("pyhugegraph.utils.util.log.error") as log_error:
check_if_success(response)
logged_args = str(log_error.call_args)
assert "server-secret" not in logged_args
assert "***REDACTED***" in logged_args
def test_redact_sensitive_data_returns_non_sensitive_string_unchanged(self):
plain_body = "large non-sensitive payload with unicode 中文"
assert redact_sensitive_data(plain_body) == plain_body
if __name__ == "__main__":
unittest.main()