mirror of https://github.com/nucypher/nucypher.git
Merge pull request #3042 from derekpierre/weird-python-chars
Fix weird python chars in exception messages/logging/REST responsespull/3043/head
commit
9ed0186060
|
@ -0,0 +1 @@
|
|||
Use decoded text from failed HTTP Responses for exception messages.
|
|
@ -1,20 +1,15 @@
|
|||
|
||||
|
||||
|
||||
import socket
|
||||
import ssl
|
||||
import time
|
||||
from http import HTTPStatus
|
||||
from pathlib import Path
|
||||
from typing import Optional, Tuple
|
||||
from typing import Sequence
|
||||
from typing import Optional, Sequence
|
||||
|
||||
import requests
|
||||
from constant_sorrow.constants import EXEMPT_FROM_VERIFICATION
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.x509 import Certificate
|
||||
from nucypher_core import MetadataRequest, FleetStateChecksum, NodeMetadata
|
||||
from nucypher_core import FleetStateChecksum, MetadataRequest, NodeMetadata
|
||||
from requests.exceptions import SSLError
|
||||
|
||||
from nucypher.blockchain.eth.registry import BaseContractRegistry
|
||||
|
@ -146,22 +141,24 @@ class NucypherMiddlewareClient:
|
|||
if cleaned_response.status_code >= 300:
|
||||
|
||||
if cleaned_response.status_code == HTTPStatus.BAD_REQUEST:
|
||||
raise RestMiddleware.BadRequest(reason=cleaned_response.content)
|
||||
raise RestMiddleware.BadRequest(reason=cleaned_response.text)
|
||||
|
||||
elif cleaned_response.status_code == HTTPStatus.NOT_FOUND:
|
||||
m = f"While trying to {method_name} {args} ({kwargs}), server 404'd. Response: {cleaned_response.content}"
|
||||
m = f"While trying to {method_name} {args} ({kwargs}), server 404'd. Response: {cleaned_response.text}"
|
||||
raise RestMiddleware.NotFound(m)
|
||||
|
||||
elif cleaned_response.status_code == HTTPStatus.PAYMENT_REQUIRED:
|
||||
# TODO: Use this as a hook to prompt Bob's payment for policy sponsorship
|
||||
# https://getyarn.io/yarn-clip/ce0d37ba-4984-4210-9a40-c9c9859a3164
|
||||
raise RestMiddleware.PaymentRequired(cleaned_response.content)
|
||||
raise RestMiddleware.PaymentRequired(cleaned_response.text)
|
||||
|
||||
elif cleaned_response.status_code == HTTPStatus.FORBIDDEN:
|
||||
raise RestMiddleware.Unauthorized(cleaned_response.content)
|
||||
raise RestMiddleware.Unauthorized(cleaned_response.text)
|
||||
|
||||
else:
|
||||
raise RestMiddleware.UnexpectedResponse(cleaned_response.content, status=cleaned_response.status_code)
|
||||
raise RestMiddleware.UnexpectedResponse(
|
||||
cleaned_response.text, status=cleaned_response.status_code
|
||||
)
|
||||
|
||||
return cleaned_response
|
||||
|
||||
|
@ -218,15 +215,18 @@ class RestMiddleware:
|
|||
super().__init__(message, *args, **kwargs)
|
||||
|
||||
class UnexpectedResponse(Exception):
|
||||
"""Based for all HTTP status codes"""
|
||||
def __init__(self, message, status, *args, **kwargs):
|
||||
super().__init__(message, *args, **kwargs)
|
||||
self.status = status
|
||||
|
||||
class NotFound(UnexpectedResponse):
|
||||
"""Raised for HTTP 404"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(status=HTTPStatus.NOT_FOUND, *args, **kwargs)
|
||||
|
||||
class BadRequest(UnexpectedResponse):
|
||||
"""Raised for HTTP 400"""
|
||||
def __init__(self, reason, *args, **kwargs):
|
||||
self.reason = reason
|
||||
super().__init__(message=reason, status=HTTPStatus.BAD_REQUEST, *args, **kwargs)
|
||||
|
|
|
@ -809,7 +809,11 @@ class Learner:
|
|||
self.cycle_teacher_node()
|
||||
|
||||
if response.status_code != 200:
|
||||
self.log.info("Bad response from teacher {}: {} - {}".format(current_teacher, response, response.content))
|
||||
self.log.info(
|
||||
"Bad response from teacher {}: {} - {}".format(
|
||||
current_teacher, response, response.text
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
# TODO: we really should be checking this *before* we ask it for a node list,
|
||||
|
@ -822,10 +826,13 @@ class Learner:
|
|||
#
|
||||
# Deserialize
|
||||
#
|
||||
response_data = response.content
|
||||
try:
|
||||
metadata = MetadataResponse.from_bytes(response.content)
|
||||
metadata = MetadataResponse.from_bytes(response_data)
|
||||
except Exception as e:
|
||||
self.log.warn(f"Failed to deserialize MetadataResponse from Teacher {current_teacher} ({e}): {response.content}")
|
||||
self.log.warn(
|
||||
f"Failed to deserialize MetadataResponse from Teacher {current_teacher} ({e}): hex bytes={response_data.hex()}"
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
|
@ -833,7 +840,8 @@ class Learner:
|
|||
except Exception as e:
|
||||
# TODO (#567): bucket the node as suspicious
|
||||
self.log.warn(
|
||||
f"Failed to verify MetadataResponse from Teacher {current_teacher} ({e}): {response.content}")
|
||||
f"Failed to verify MetadataResponse from Teacher {current_teacher} ({e}): hex bytes={response_data.hex()}"
|
||||
)
|
||||
return
|
||||
|
||||
# End edge case handling.
|
||||
|
|
|
@ -163,7 +163,7 @@ def evaluate_condition_lingo(
|
|||
)
|
||||
except NoConnectionToChain as e:
|
||||
error = EvalError(
|
||||
f"Node does not have a connection to chain ID {e.chain}: {e}",
|
||||
f"Node does not have a connection to chain ID {e.chain}",
|
||||
HTTPStatus.NOT_IMPLEMENTED,
|
||||
)
|
||||
except ConditionEvaluationFailed as e:
|
||||
|
|
|
@ -4,6 +4,7 @@ import pytest
|
|||
from nucypher_core import Conditions
|
||||
|
||||
from nucypher.characters.lawful import Ursula
|
||||
from nucypher.policy.conditions.exceptions import *
|
||||
from nucypher.policy.conditions.lingo import ConditionLingo
|
||||
from tests.utils.middleware import MockRestMiddleware
|
||||
|
||||
|
@ -24,9 +25,12 @@ def test_single_retrieve_with_truthy_conditions(enacted_policy, bob, ursulas, mo
|
|||
bob.start_learning_loop()
|
||||
|
||||
conditions = [
|
||||
{'returnValueTest': {'value': '0', 'comparator': '>'}, 'method': 'timelock'},
|
||||
{'operator': 'and'},
|
||||
{'returnValueTest': {'value': '99999999999999999', 'comparator': '<'}, 'method': 'timelock'},
|
||||
{"returnValueTest": {"value": 0, "comparator": ">"}, "method": "timelock"},
|
||||
{"operator": "and"},
|
||||
{
|
||||
"returnValueTest": {"value": 99999999999999999, "comparator": "<"},
|
||||
"method": "timelock",
|
||||
},
|
||||
]
|
||||
json_conditions = json.dumps(conditions)
|
||||
rust_conditions = Conditions(json_conditions)
|
||||
|
@ -49,9 +53,11 @@ def test_single_retrieve_with_falsy_conditions(enacted_policy, bob, ursulas, moc
|
|||
reencrypt_http_spy = mocker.spy(MockRestMiddleware, 'reencrypt')
|
||||
|
||||
# not actually used for eval, but satisfies serializers
|
||||
conditions = Conditions(json.dumps(
|
||||
[{'returnValueTest': {'value': '0', 'comparator': '>'}, 'method': 'timelock'}]
|
||||
))
|
||||
conditions = Conditions(
|
||||
json.dumps(
|
||||
[{"returnValueTest": {"value": 0, "comparator": ">"}, "method": "timelock"}]
|
||||
)
|
||||
)
|
||||
|
||||
bob.start_learning_loop()
|
||||
|
||||
|
@ -65,3 +71,80 @@ def test_single_retrieve_with_falsy_conditions(enacted_policy, bob, ursulas, moc
|
|||
|
||||
reencrypt_spy.assert_not_called()
|
||||
assert isinstance(reencrypt_http_spy.spy_exception, MockRestMiddleware.Unauthorized)
|
||||
|
||||
|
||||
FAILURE_MESSAGE = "I’ve failed over and over and over again in my life. And that is why I succeed." # -- Michael Jordan
|
||||
|
||||
FAILURE_CASE_EXCEPTION_CODE_MATCHING = [
|
||||
# (condition exception class, exception parameters, middleware exception class)
|
||||
(ReturnValueEvaluationError, MockRestMiddleware.BadRequest),
|
||||
(InvalidConditionLingo, MockRestMiddleware.BadRequest),
|
||||
(InvalidCondition, MockRestMiddleware.BadRequest),
|
||||
(RequiredContextVariable, MockRestMiddleware.BadRequest),
|
||||
(InvalidContextVariableData, MockRestMiddleware.BadRequest),
|
||||
(ContextVariableVerificationFailed, MockRestMiddleware.Unauthorized),
|
||||
(NoConnectionToChain, MockRestMiddleware.UnexpectedResponse),
|
||||
(ConditionEvaluationFailed, MockRestMiddleware.BadRequest),
|
||||
(ValueError, MockRestMiddleware.UnexpectedResponse),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"eval_failure_exception_class, middleware_exception_class",
|
||||
FAILURE_CASE_EXCEPTION_CODE_MATCHING,
|
||||
)
|
||||
def test_middleware_handling_of_failed_condition_responses(
|
||||
eval_failure_exception_class,
|
||||
middleware_exception_class,
|
||||
mocker,
|
||||
enacted_policy,
|
||||
bob,
|
||||
mock_rest_middleware,
|
||||
):
|
||||
# we use a failed condition for reencryption to test conversion of response codes to middleware exceptions
|
||||
from nucypher_core import MessageKit
|
||||
|
||||
reencrypt_http_spy = mocker.spy(MockRestMiddleware, "reencrypt")
|
||||
|
||||
# not actually used for eval, but satisfies serializers
|
||||
conditions = Conditions(
|
||||
json.dumps(
|
||||
[
|
||||
{
|
||||
"returnValueTest": {"value": 0, "comparator": ">"},
|
||||
"method": "timelock",
|
||||
}
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
bob.start_learning_loop()
|
||||
|
||||
message_kits = [MessageKit(enacted_policy.public_key, b"radio", conditions)]
|
||||
|
||||
# use string message or chain id as exception parameter
|
||||
chain_id = 1
|
||||
exception_parameter = (
|
||||
FAILURE_MESSAGE
|
||||
if eval_failure_exception_class != NoConnectionToChain
|
||||
else chain_id
|
||||
)
|
||||
mocker.patch.object(
|
||||
ConditionLingo,
|
||||
"eval",
|
||||
side_effect=eval_failure_exception_class(exception_parameter),
|
||||
)
|
||||
|
||||
with pytest.raises(Ursula.NotEnoughUrsulas):
|
||||
# failed retrieval because of failed exception
|
||||
bob.retrieve_and_decrypt(
|
||||
message_kits=message_kits,
|
||||
encrypted_treasure_map=enacted_policy.treasure_map,
|
||||
alice_verifying_key=enacted_policy.publisher_verifying_key,
|
||||
)
|
||||
|
||||
actual_exception = reencrypt_http_spy.spy_exception
|
||||
assert type(actual_exception) == middleware_exception_class # be specific
|
||||
# verify message is not in bytes form
|
||||
assert "b'" not in str(actual_exception) # no byte string included in message
|
||||
assert str(exception_parameter) in str(actual_exception)
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
from http import HTTPStatus
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.utils.middleware import MockRestMiddleware
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"status_code, expected_exception_class",
|
||||
[
|
||||
(HTTPStatus.BAD_REQUEST, MockRestMiddleware.BadRequest),
|
||||
(HTTPStatus.NOT_FOUND, MockRestMiddleware.NotFound),
|
||||
(HTTPStatus.PAYMENT_REQUIRED, MockRestMiddleware.PaymentRequired),
|
||||
(HTTPStatus.FORBIDDEN, MockRestMiddleware.Unauthorized),
|
||||
# catch alls
|
||||
(HTTPStatus.REQUEST_TIMEOUT, MockRestMiddleware.UnexpectedResponse),
|
||||
(HTTPStatus.INTERNAL_SERVER_ERROR, MockRestMiddleware.UnexpectedResponse),
|
||||
(HTTPStatus.NOT_IMPLEMENTED, MockRestMiddleware.UnexpectedResponse),
|
||||
(HTTPStatus.SERVICE_UNAVAILABLE, MockRestMiddleware.UnexpectedResponse),
|
||||
],
|
||||
)
|
||||
def test_middleware_response_status_code_processing(
|
||||
status_code,
|
||||
expected_exception_class,
|
||||
mocker,
|
||||
mock_rest_middleware,
|
||||
ursulas,
|
||||
):
|
||||
ursula = list(ursulas)[0]
|
||||
_original_execute_method = mock_rest_middleware.client._execute_method
|
||||
|
||||
def execute_method_side_effect(
|
||||
node_or_sprout, host, port, method, endpoint, *args, **kwargs
|
||||
):
|
||||
endpoint_url = urlparse(endpoint)
|
||||
if endpoint_url.path == "/reencrypt":
|
||||
response = mocker.MagicMock(
|
||||
text="I have not failed. I've just found 10,000 ways that won't work.", # -- Thomas Edison
|
||||
status_code=status_code,
|
||||
)
|
||||
return response
|
||||
else:
|
||||
return _original_execute_method(
|
||||
node_or_sprout, host, port, method, endpoint, *args, **kwargs
|
||||
)
|
||||
|
||||
mocker.patch.object(
|
||||
mock_rest_middleware.client,
|
||||
"_execute_method",
|
||||
side_effect=execute_method_side_effect,
|
||||
)
|
||||
with pytest.raises(expected_exception_class):
|
||||
mock_rest_middleware.reencrypt(
|
||||
ursula=ursula, reencryption_request_bytes=b"reencryption_request"
|
||||
)
|
Loading…
Reference in New Issue