deb-python-autobahn/autobahn/wamp/test/test_user_handler_errors.py

403 lines
16 KiB
Python

###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) Crossbar.io Technologies GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
from __future__ import absolute_import
import os
if os.environ.get('USE_TWISTED', False):
from twisted.trial import unittest
from twisted.internet import defer
from twisted.python.failure import Failure
from autobahn.wamp import message, role
from autobahn.wamp.exception import ProtocolError
from autobahn.twisted.wamp import ApplicationSession
class MockTransport:
def __init__(self):
self.messages = []
def send(self, msg):
self.messages.append(msg)
def close(self, *args, **kw):
pass
class MockApplicationSession(ApplicationSession):
'''
This is used by tests, which typically attach their own handler to
on*() methods. This just collects any errors from onUserError
'''
def __init__(self, *args, **kw):
ApplicationSession.__init__(self, *args, **kw)
self.errors = []
self._realm = u'dummy'
self._transport = MockTransport()
def onUserError(self, e, msg):
self.errors.append((e.value, msg))
def exception_raiser(exc):
'''
Create a method that takes any args and always raises the given
Exception instance.
'''
assert isinstance(exc, Exception), "Must derive from Exception"
def method(*args, **kw):
raise exc
return method
def async_exception_raiser(exc):
'''
Create a method that takes any args, and always returns a Deferred
that has failed.
'''
assert isinstance(exc, Exception), "Must derive from Exception"
def method(*args, **kw):
try:
raise exc
except:
return defer.fail(Failure())
return method
def create_mock_welcome():
return message.Welcome(
1234,
{
u'broker': role.RoleBrokerFeatures(),
},
)
class TestSessionCallbacks(unittest.TestCase):
'''
These test that callbacks on user-overridden ApplicationSession
methods that produce errors are handled correctly.
XXX should do state-diagram documenting where we are when each
of these cases arises :/
'''
# XXX sure would be nice to use py.test @fixture to do the
# async/sync exception-raising stuff (i.e. make each test run
# twice)...but that would mean switching all test-running over
# to py-test
skip = True
def test_on_join(self):
session = MockApplicationSession()
exception = RuntimeError("blammo")
session.onJoin = exception_raiser(exception)
msg = create_mock_welcome()
# give the sesion a WELCOME, from which it should call onJoin
session.onMessage(msg)
# make sure we got the right error out of onUserError
self.assertEqual(1, len(session.errors))
self.assertEqual(exception, session.errors[0][0])
def test_on_join_deferred(self):
session = MockApplicationSession()
exception = RuntimeError("blammo")
session.onJoin = async_exception_raiser(exception)
msg = create_mock_welcome()
# give the sesion a WELCOME, from which it should call onJoin
session.onMessage(msg)
# make sure we got the right error out of onUserError
# import traceback
# traceback.print_exception(*session.errors[0][:3])
self.assertEqual(1, len(session.errors))
self.assertEqual(exception, session.errors[0][0])
def test_on_leave(self):
session = MockApplicationSession()
exception = RuntimeError("boom")
session.onLeave = exception_raiser(exception)
msg = message.Abort(u"testing")
# we haven't done anything, so this is "abort before we've
# connected"
session.onMessage(msg)
# make sure we got the right error out of onUserError
self.assertEqual(1, len(session.errors))
self.assertEqual(exception, session.errors[0][0])
def test_on_leave_deferred(self):
session = MockApplicationSession()
exception = RuntimeError("boom")
session.onLeave = async_exception_raiser(exception)
msg = message.Abort(u"testing")
# we haven't done anything, so this is "abort before we've
# connected"
session.onMessage(msg)
# make sure we got the right error out of onUserError
self.assertEqual(1, len(session.errors))
self.assertEqual(exception, session.errors[0][0])
def test_on_leave_valid_session(self):
'''
cover when onLeave called after we have a valid session
'''
session = MockApplicationSession()
exception = RuntimeError("such challenge")
session.onLeave = exception_raiser(exception)
# we have to get to an established connection first...
session.onMessage(create_mock_welcome())
self.assertTrue(session._session_id is not None)
# okay we have a session ("because ._session_id is not None")
msg = message.Goodbye()
session.onMessage(msg)
self.assertEqual(1, len(session.errors))
self.assertEqual(exception, session.errors[0][0])
def test_on_leave_valid_session_deferred(self):
'''
cover when onLeave called after we have a valid session
'''
session = MockApplicationSession()
exception = RuntimeError("such challenge")
session.onLeave = async_exception_raiser(exception)
# we have to get to an established connection first...
session.onMessage(create_mock_welcome())
self.assertTrue(session._session_id is not None)
# okay we have a session ("because ._session_id is not None")
msg = message.Goodbye()
session.onMessage(msg)
self.assertEqual(1, len(session.errors))
self.assertEqual(exception, session.errors[0][0])
def test_on_leave_after_bad_challenge(self):
'''
onLeave raises error after onChallenge fails
'''
session = MockApplicationSession()
exception = RuntimeError("such challenge")
session.onLeave = exception_raiser(exception)
session.onChallenge = exception_raiser(exception)
# make a challenge (which will fail, and then the
# subsequent onLeave will also fail)
msg = message.Challenge(u"foo")
session.onMessage(msg)
self.assertEqual(2, len(session.errors))
self.assertEqual(exception, session.errors[0][0])
def test_on_disconnect_via_close(self):
session = MockApplicationSession()
exception = RuntimeError("sideways")
session.onDisconnect = exception_raiser(exception)
# we short-cut the whole state-machine traversal here by
# just calling onClose directly, which would normally be
# called via a Protocol, e.g.,
# autobahn.wamp.websocket.WampWebSocketProtocol
session.onClose(False)
self.assertEqual(1, len(session.errors))
self.assertEqual(exception, session.errors[0][0])
def test_on_disconnect_via_close_deferred(self):
session = MockApplicationSession()
exception = RuntimeError("sideways")
session.onDisconnect = async_exception_raiser(exception)
# we short-cut the whole state-machine traversal here by
# just calling onClose directly, which would normally be
# called via a Protocol, e.g.,
# autobahn.wamp.websocket.WampWebSocketProtocol
session.onClose(False)
self.assertEqual(1, len(session.errors))
self.assertEqual(exception, session.errors[0][0])
# XXX FIXME Probably more ways to call onLeave!
def test_on_challenge(self):
session = MockApplicationSession()
exception = RuntimeError("such challenge")
session.onChallenge = exception_raiser(exception)
msg = message.Challenge(u"foo")
# execute
session.onMessage(msg)
# we already handle any onChallenge errors as "abort the
# connection". So make sure our error showed up in the
# fake-transport.
self.assertEqual(1, len(session.errors))
self.assertEqual(exception, session.errors[0][0])
self.assertEqual(1, len(session._transport.messages))
reply = session._transport.messages[0]
self.assertIsInstance(reply, message.Abort)
self.assertEqual("such challenge", reply.message)
def test_on_challenge_deferred(self):
session = MockApplicationSession()
exception = RuntimeError("such challenge")
session.onChallenge = async_exception_raiser(exception)
msg = message.Challenge(u"foo")
# execute
session.onMessage(msg)
# we already handle any onChallenge errors as "abort the
# connection". So make sure our error showed up in the
# fake-transport.
self.assertEqual(1, len(session.errors))
self.assertEqual(session.errors[0][0], exception)
self.assertEqual(1, len(session._transport.messages))
reply = session._transport.messages[0]
self.assertIsInstance(reply, message.Abort)
self.assertEqual("such challenge", reply.message)
def test_no_session(self):
'''
test "all other cases" when we don't yet have a session
established, which should all raise ProtocolErrors and
*not* go through the onUserError handler. We cheat and
just test one.
'''
session = MockApplicationSession()
exception = RuntimeError("such challenge")
session.onConnect = exception_raiser(exception)
for msg in [message.Goodbye()]:
self.assertRaises(ProtocolError, session.onMessage, (msg,))
self.assertEqual(0, len(session.errors))
def test_on_disconnect(self):
session = MockApplicationSession()
exception = RuntimeError("oh sadness")
session.onDisconnect = exception_raiser(exception)
# we short-cut the whole state-machine traversal here by
# just calling onClose directly, which would normally be
# called via a Protocol, e.g.,
# autobahn.wamp.websocket.WampWebSocketProtocol
session.onClose(False)
self.assertEqual(1, len(session.errors))
self.assertEqual(exception, session.errors[0][0])
def test_on_disconnect_deferred(self):
session = MockApplicationSession()
exception = RuntimeError("oh sadness")
session.onDisconnect = async_exception_raiser(exception)
# we short-cut the whole state-machine traversal here by
# just calling onClose directly, which would normally be
# called via a Protocol, e.g.,
# autobahn.wamp.websocket.WampWebSocketProtocol
session.onClose(False)
self.assertEqual(1, len(session.errors))
self.assertEqual(exception, session.errors[0][0])
def test_on_disconnect_with_session(self):
session = MockApplicationSession()
exception = RuntimeError("the pain runs deep")
session.onDisconnect = exception_raiser(exception)
# create a valid session
session.onMessage(create_mock_welcome())
# we short-cut the whole state-machine traversal here by
# just calling onClose directly, which would normally be
# called via a Protocol, e.g.,
# autobahn.wamp.websocket.WampWebSocketProtocol
session.onClose(False)
self.assertEqual(1, len(session.errors))
self.assertEqual(exception, session.errors[0][0])
def test_on_disconnect_with_session_deferred(self):
session = MockApplicationSession()
exception = RuntimeError("the pain runs deep")
session.onDisconnect = async_exception_raiser(exception)
# create a valid session
session.onMessage(create_mock_welcome())
# we short-cut the whole state-machine traversal here by
# just calling onClose directly, which would normally be
# called via a Protocol, e.g.,
# autobahn.wamp.websocket.WampWebSocketProtocol
session.onClose(False)
self.assertEqual(1, len(session.errors))
self.assertEqual(exception, session.errors[0][0])
def test_on_connect(self):
session = MockApplicationSession()
exception = RuntimeError("the pain runs deep")
session.onConnect = exception_raiser(exception)
trans = MockTransport()
# normally would be called from a Protocol?
session.onOpen(trans)
# shouldn't have done the .join()
self.assertEqual(0, len(trans.messages))
self.assertEqual(1, len(session.errors))
self.assertEqual(exception, session.errors[0][0])
def test_on_connect_deferred(self):
session = MockApplicationSession()
exception = RuntimeError("the pain runs deep")
session.onConnect = async_exception_raiser(exception)
trans = MockTransport()
# normally would be called from a Protocol?
session.onOpen(trans)
# shouldn't have done the .join()
self.assertEqual(0, len(trans.messages))
self.assertEqual(1, len(session.errors))
self.assertEqual(exception, session.errors[0][0])
# XXX likely missing other ways to invoke the above. need to
# cover, for sure:
#
# onChallenge
# onJoin
# onLeave
# onDisconnect
#
# what about other ISession ones?
# onConnect
# onDisconnect
# NOTE: for Event stuff, that is publish() handlers,
# test_publish_callback_exception in test_protocol.py already
# covers exceptions coming from user-code.