173 lines
4.9 KiB
Python
173 lines
4.9 KiB
Python
import traceback
|
|
|
|
import pytest
|
|
from OpenSSL import SSL
|
|
|
|
from smartthings_local.errors import (
|
|
AuthenticationError,
|
|
AuthorizationError,
|
|
BlockwiseError,
|
|
EndpointError,
|
|
MalformedMessageError,
|
|
ObserveError,
|
|
ProbeError,
|
|
SessionClosedError,
|
|
SessionError,
|
|
SessionTimeoutError,
|
|
SmartThingsLocalError,
|
|
)
|
|
from smartthings_local.protocol import dtls_session
|
|
from smartthings_local.protocol.dtls_session import DtlsCoapSession
|
|
from smartthings_local.protocol.endpoint import ResolvedUdpEndpoint
|
|
|
|
ERROR_TYPES = (
|
|
EndpointError,
|
|
ProbeError,
|
|
SessionError,
|
|
AuthenticationError,
|
|
AuthorizationError,
|
|
SessionTimeoutError,
|
|
SessionClosedError,
|
|
MalformedMessageError,
|
|
BlockwiseError,
|
|
ObserveError,
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
('error_type', 'legacy_type'),
|
|
(
|
|
(EndpointError, OSError),
|
|
(ProbeError, ConnectionError),
|
|
(SessionError, ConnectionError),
|
|
(AuthenticationError, ConnectionError),
|
|
(AuthorizationError, PermissionError),
|
|
(SessionTimeoutError, TimeoutError),
|
|
(SessionClosedError, ConnectionError),
|
|
(MalformedMessageError, ValueError),
|
|
(BlockwiseError, ConnectionError),
|
|
(ObserveError, ConnectionError),
|
|
),
|
|
)
|
|
def test_errors_preserve_legacy_builtin_catches(error_type, legacy_type):
|
|
error = error_type()
|
|
assert isinstance(error, SmartThingsLocalError)
|
|
assert isinstance(error, legacy_type)
|
|
|
|
|
|
@pytest.mark.parametrize('error_type', ERROR_TYPES)
|
|
def test_error_text_is_fixed_and_redacted(error_type):
|
|
error = error_type()
|
|
text = f'{error!s} {error!r}'
|
|
assert error.code
|
|
assert str(error) == error.message
|
|
assert repr(error) == f'{error_type.__name__}(code={error.code!r})'
|
|
assert 'device.example' not in text
|
|
assert '/synthetic/client.key' not in text
|
|
assert 'credential-value' not in text
|
|
with pytest.raises(TypeError):
|
|
error_type('credential-value')
|
|
|
|
|
|
def test_chained_backend_error_is_not_copied_into_public_text():
|
|
backend = ConnectionError('DTLS backend failed')
|
|
|
|
try:
|
|
raise SessionError() from backend
|
|
except SessionError as error:
|
|
assert error.__cause__ is backend
|
|
formatted = ''.join(traceback.format_exception(error))
|
|
assert 'DTLS backend failed' in formatted
|
|
assert 'device.example' not in formatted
|
|
assert 'credential-value' not in formatted
|
|
|
|
|
|
def test_handshake_error_is_classified_without_backend_text(monkeypatch):
|
|
class FakeContext:
|
|
def load_verify_locations(self, *args):
|
|
pass
|
|
|
|
def set_verify(self, *args):
|
|
pass
|
|
|
|
def set_cipher_list(self, *args):
|
|
pass
|
|
|
|
def use_certificate_chain_file(self, *args):
|
|
pass
|
|
|
|
def use_privatekey_file(self, *args):
|
|
pass
|
|
|
|
def check_privatekey(self):
|
|
pass
|
|
|
|
class FakeConnection:
|
|
def set_connect_state(self):
|
|
pass
|
|
|
|
def set_ciphertext_mtu(self, *args):
|
|
pass
|
|
|
|
def do_handshake(self):
|
|
raise SSL.Error('credential-value at device.example')
|
|
|
|
class FakeSocket:
|
|
closed = False
|
|
|
|
def settimeout(self, *args):
|
|
pass
|
|
|
|
def close(self):
|
|
self.closed = True
|
|
|
|
fake_socket = FakeSocket()
|
|
monkeypatch.setattr(dtls_session.SSL, 'Context', lambda *args: FakeContext())
|
|
monkeypatch.setattr(
|
|
dtls_session.SSL, 'Connection', lambda *args: FakeConnection())
|
|
endpoint = ResolvedUdpEndpoint(
|
|
dtls_session.socket.AF_INET, ('192.0.2.10', 5684))
|
|
monkeypatch.setattr(
|
|
dtls_session,
|
|
'open_connected_udp_socket',
|
|
lambda *args, **kwargs: (fake_socket, endpoint),
|
|
)
|
|
|
|
session = DtlsCoapSession(
|
|
'device.example', 5684,
|
|
cert_path='/synthetic/client.pem',
|
|
key_path='/synthetic/client.key',
|
|
)
|
|
with pytest.raises(SessionError) as exc:
|
|
session.connect()
|
|
|
|
assert fake_socket.closed
|
|
assert isinstance(exc.value, ConnectionError)
|
|
assert isinstance(exc.value.__cause__, ConnectionError)
|
|
assert exc.value.__context__ is None
|
|
formatted = ''.join(traceback.format_exception(exc.value))
|
|
assert 'DTLS backend failed' in formatted
|
|
assert 'device.example' not in formatted
|
|
assert 'credential-value' not in formatted
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
'operation',
|
|
(
|
|
lambda session: session.get(['resource']),
|
|
lambda session: session.post(['resource'], b'payload'),
|
|
lambda session: session.ping(),
|
|
lambda session: session.refresh_observes([]),
|
|
lambda session: session.subscribe(['resource']),
|
|
),
|
|
)
|
|
def test_closed_session_operations_raise_classified_error(operation):
|
|
session = DtlsCoapSession(
|
|
'device.example', 5684,
|
|
cert_path='/synthetic/client.pem',
|
|
key_path='/synthetic/client.key',
|
|
)
|
|
|
|
with pytest.raises(SessionClosedError):
|
|
operation(session)
|