403 lines
16 KiB
Python
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.
|