Initial Commit
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int 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.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
import unittest
|
||||
from unittest.mock import Mock
|
||||
import platform
|
||||
|
||||
import re
|
||||
import json
|
||||
import binascii
|
||||
|
||||
from autobahn.wamp import auth
|
||||
from autobahn.wamp import types
|
||||
|
||||
|
||||
# these test vectors are all for HMAC-SHA1
|
||||
PBKDF2_TEST_VECTORS = [
|
||||
# From RFC 6070
|
||||
(b'password', b'salt', 1, 20, '0c60c80f961f0e71f3a9b524af6012062fe037a6'),
|
||||
(b'password', b'salt', 2, 20, 'ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957'),
|
||||
|
||||
# From Crypt-PBKDF2
|
||||
(b'password', b'ATHENA.MIT.EDUraeburn', 1, 16, 'cdedb5281bb2f801565a1122b2563515'),
|
||||
(b'password', b'ATHENA.MIT.EDUraeburn', 1, 32, 'cdedb5281bb2f801565a1122b25635150ad1f7a04bb9f3a333ecc0e2e1f70837'),
|
||||
(b'password', b'ATHENA.MIT.EDUraeburn', 2, 16, '01dbee7f4a9e243e988b62c73cda935d'),
|
||||
(b'password', b'ATHENA.MIT.EDUraeburn', 2, 32, '01dbee7f4a9e243e988b62c73cda935da05378b93244ec8f48a99e61ad799d86'),
|
||||
(b'password', b'ATHENA.MIT.EDUraeburn', 1200, 32, '5c08eb61fdf71e4e4ec3cf6ba1f5512ba7e52ddbc5e5142f708a31e2e62b1e13'),
|
||||
(b'X' * 64, b'pass phrase equals block size', 1200, 32, '139c30c0966bc32ba55fdbf212530ac9c5ec59f1a452f5cc9ad940fea0598ed1'),
|
||||
(b'X' * 65, b'pass phrase exceeds block size', 1200, 32, '9ccad6d468770cd51b10e6a68721be611a8b4d282601db3b36be9246915ec82a'),
|
||||
]
|
||||
|
||||
if platform.python_implementation() != 'PyPy':
|
||||
|
||||
# the following fails on PyPy: "RuntimeError: maximum recursion depth exceeded"
|
||||
PBKDF2_TEST_VECTORS.extend(
|
||||
[
|
||||
# From RFC 6070
|
||||
(b'password', b'salt', 4096, 20, '4b007901b765489abead49d926f721d065a429c1'),
|
||||
(b'passwordPASSWORDpassword', b'saltSALTsaltSALTsaltSALTsaltSALTsalt', 4096, 25, '3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038'),
|
||||
(b'pass\x00word', b'sa\x00lt', 4096, 16, '56fa6aa75548099dcc37d7f03425e0c3'),
|
||||
|
||||
# This one is from the RFC but it just takes for ages
|
||||
# (b'password', b'salt', 16777216, 20, 'eefe3d61cd4da4e4e9945b3d6ba2158c2634e984'),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class TestWampAuthHelpers(unittest.TestCase):
|
||||
|
||||
def test_pbkdf2(self):
|
||||
for tv in PBKDF2_TEST_VECTORS:
|
||||
result = auth.pbkdf2(tv[0], tv[1], tv[2], tv[3], 'sha1')
|
||||
self.assertEqual(type(result), bytes)
|
||||
self.assertEqual(binascii.hexlify(result).decode('ascii'), tv[4])
|
||||
|
||||
def test_generate_totp_secret_default(self):
|
||||
secret = auth.generate_totp_secret()
|
||||
self.assertEqual(type(secret), str)
|
||||
self.assertEqual(len(secret), 10 * 8 / 5)
|
||||
|
||||
def test_generate_totp_secret_length(self):
|
||||
for length in [5, 10, 20, 30, 40, 50]:
|
||||
secret = auth.generate_totp_secret(length)
|
||||
self.assertEqual(type(secret), str)
|
||||
self.assertEqual(len(secret), length * 8 / 5)
|
||||
|
||||
def test_compute_totp(self):
|
||||
pat = re.compile(r"\d\d\d\d\d\d")
|
||||
secret = "MFRGGZDFMZTWQ2LK"
|
||||
signature = auth.compute_totp(secret)
|
||||
self.assertEqual(type(signature), str)
|
||||
self.assertTrue(pat.match(signature) is not None)
|
||||
|
||||
def test_compute_totp_offset(self):
|
||||
pat = re.compile(r"\d\d\d\d\d\d")
|
||||
secret = "MFRGGZDFMZTWQ2LK"
|
||||
for offset in range(-10, 10):
|
||||
signature = auth.compute_totp(secret, offset)
|
||||
self.assertEqual(type(signature), str)
|
||||
self.assertTrue(pat.match(signature) is not None)
|
||||
|
||||
def test_derive_key(self):
|
||||
secret = 'L3L1YUE8Txlw'
|
||||
salt = 'salt123'
|
||||
key = auth.derive_key(secret.encode('utf8'), salt.encode('utf8'))
|
||||
self.assertEqual(type(key), bytes)
|
||||
self.assertEqual(key, b"qzcdsr9uu/L5hnss3kjNTRe490ETgA70ZBaB5rvnJ5Y=")
|
||||
|
||||
def test_generate_wcs_default(self):
|
||||
secret = auth.generate_wcs()
|
||||
self.assertEqual(type(secret), bytes)
|
||||
self.assertEqual(len(secret), 14)
|
||||
|
||||
def test_generate_wcs_length(self):
|
||||
for length in [5, 10, 20, 30, 40, 50]:
|
||||
secret = auth.generate_wcs(length)
|
||||
self.assertEqual(type(secret), bytes)
|
||||
self.assertEqual(len(secret), length)
|
||||
|
||||
def test_compute_wcs(self):
|
||||
secret = 'L3L1YUE8Txlw'
|
||||
challenge = json.dumps([1, 2, 3], ensure_ascii=False).encode('utf8')
|
||||
signature = auth.compute_wcs(secret.encode('utf8'), challenge)
|
||||
self.assertEqual(type(signature), bytes)
|
||||
self.assertEqual(signature, b"1njQtmmeYO41N5EWEzD2kAjjEKRZ5kPZt/TzpYXOzR0=")
|
||||
|
||||
|
||||
@unittest.skipIf(not auth.HAS_ARGON, 'no Argon2 library')
|
||||
class TestScram(unittest.TestCase):
|
||||
|
||||
def test_argon2id_static(self):
|
||||
# re-generate from the official argon2 tools:
|
||||
# echo -n "p4ssw0rd" | argon2 '1234567890abcdef' -id -t 32 -m 9 -p 1 -l 32
|
||||
expected = binascii.unhexlify('ee4a8acf9d5958354fb79a95ae20692d05e42591ba49fae85eb6700e8b0ed293')
|
||||
raw_hash = auth._hash_argon2id13_secret(
|
||||
b'p4ssw0rd',
|
||||
binascii.b2a_base64(b'1234567890abcdef'), # ours takes base64-encoded salt
|
||||
32, # this is WAY TOO SMALL; for production, use 4096 or higher
|
||||
512, # note that the argon2 utility takes a "power of 2", so "-m 9" above == 512
|
||||
)
|
||||
decoded_hash = binascii.a2b_base64(raw_hash + b'==\n')
|
||||
self.assertEqual(expected, decoded_hash)
|
||||
|
||||
def test_pbkdf2_static(self):
|
||||
expected = binascii.unhexlify('f6991a28c75f43751e0d75499fd7b8649f659118ddc1d61cee5883af547d15f5')
|
||||
# 8 iterations is WAY TOO FEW for production; this is a test
|
||||
raw_hash = auth._hash_pbkdf2_secret(b'p4ssw0rd', b'1234567890abcdef', 8)
|
||||
self.assertEqual(raw_hash, expected)
|
||||
|
||||
def test_basic(self):
|
||||
scram = auth.AuthScram(
|
||||
nonce='1234567890abcdef',
|
||||
kdf='argon2id13',
|
||||
salt=binascii.b2a_hex(b'1234567890abcdef').decode('ascii'),
|
||||
iterations=32, # far too few; use 4096 or more for production
|
||||
memory=512,
|
||||
password='p4ssw0rd',
|
||||
authid='username',
|
||||
)
|
||||
# thought: if we could import crossbar code here, we could
|
||||
# test the "other side" of this with fewer mocks
|
||||
# (i.e. hard-coding the client nonce)
|
||||
scram._client_nonce = binascii.b2a_hex(b'1234567890abcdef').decode('ascii')
|
||||
self.assertEqual(
|
||||
{'nonce': '31323334353637383930616263646566'},
|
||||
scram.authextra,
|
||||
)
|
||||
|
||||
challenge = types.Challenge('scram', {
|
||||
'nonce': '1234567890abcdeffedcba0987654321',
|
||||
'kdf': 'argon2id-13',
|
||||
'salt': binascii.b2a_hex(b'1234567890abcdef').decode('ascii'),
|
||||
'iterations': 32,
|
||||
'memory': 512,
|
||||
})
|
||||
reply = scram.on_challenge(Mock(), challenge)
|
||||
self.assertEqual(
|
||||
b'f5r3loERzGVSuimE+lvO0bWna2zyswBo0HrZkaaEy38=',
|
||||
reply,
|
||||
)
|
||||
|
||||
authextra = dict(
|
||||
scram_server_signature=b'f5r3loERzGVSuimE+lvO0bWna2zyswBo0HrZkaaEy38=',
|
||||
)
|
||||
scram.on_welcome(Mock(), authextra)
|
||||
|
||||
def test_no_memory_arg(self):
|
||||
scram = auth.AuthScram(
|
||||
nonce='1234567890abcdef',
|
||||
kdf='argon2id13',
|
||||
salt=binascii.b2a_hex(b'1234567890abcdef').decode('ascii'),
|
||||
iterations=4096,
|
||||
memory=512,
|
||||
password='p4ssw0rd',
|
||||
authid='username',
|
||||
)
|
||||
scram.authextra
|
||||
|
||||
with self.assertRaises(ValueError) as ctx:
|
||||
challenge = types.Challenge('scram', {
|
||||
'nonce': '1234567890abcdeffedcba0987654321',
|
||||
'kdf': 'argon2id-13',
|
||||
'salt': binascii.b2a_hex(b'1234567890abcdef'),
|
||||
'iterations': 4096,
|
||||
# no 'memory' key
|
||||
})
|
||||
scram.on_challenge(Mock(), challenge)
|
||||
self.assertIn(
|
||||
"requires 'memory' parameter",
|
||||
str(ctx.exception)
|
||||
)
|
||||
|
||||
def test_unknown_arg(self):
|
||||
scram = auth.AuthScram(
|
||||
nonce='1234567890abcdef',
|
||||
kdf='argon2id13',
|
||||
salt=binascii.b2a_hex(b'1234567890abcdef'),
|
||||
iterations=4096,
|
||||
memory=512,
|
||||
password='p4ssw0rd',
|
||||
authid='username',
|
||||
)
|
||||
scram.authextra
|
||||
|
||||
with self.assertRaises(RuntimeError) as ctx:
|
||||
challenge = types.Challenge('scram', {
|
||||
'nonce': '1234567890abcdeffedcba0987654321',
|
||||
'kdf': 'argon2id-13',
|
||||
'salt': binascii.b2a_hex(b'1234567890abcdef'),
|
||||
'iterations': 4096,
|
||||
'memory': 512,
|
||||
'an_invalid_key': None
|
||||
})
|
||||
scram.on_challenge(Mock(), challenge)
|
||||
self.assertIn("an_invalid_key", str(ctx.exception))
|
||||
@@ -0,0 +1,219 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int 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.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
import os
|
||||
|
||||
if os.environ.get('USE_TWISTED', False):
|
||||
from autobahn.twisted.util import sleep
|
||||
from autobahn.twisted import wamp
|
||||
|
||||
from twisted.trial import unittest
|
||||
from twisted.internet import defer
|
||||
from twisted.application import service
|
||||
|
||||
class CaseComponent(wamp.ApplicationSession):
|
||||
"""
|
||||
Application code goes here. This is an example component that calls
|
||||
a remote procedure on a WAMP peer, subscribes to a topic to receive
|
||||
events, and then stops the world after some events.
|
||||
"""
|
||||
|
||||
def __init__(self, config):
|
||||
wamp.ApplicationSession.__init__(self, config)
|
||||
self.test = config.extra['test']
|
||||
self.stop = False
|
||||
self._logline = 1
|
||||
self.finished = False
|
||||
|
||||
def log(self, *args):
|
||||
if len(args) > 1:
|
||||
sargs = ", ".join(str(s) for s in args)
|
||||
elif len(args) == 1:
|
||||
sargs = args[0]
|
||||
else:
|
||||
sargs = "-"
|
||||
|
||||
msg = '= : {0:>3} : {1:<20} : {2}'.format(self._logline, self.__class__.__name__, sargs)
|
||||
self._logline += 1
|
||||
print(msg)
|
||||
|
||||
def finish(self):
|
||||
if not self.finished:
|
||||
self.test.deferred.callback(None)
|
||||
self.finished = True
|
||||
else:
|
||||
print("already finished")
|
||||
|
||||
class Case1_Backend(CaseComponent):
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def onJoin(self, details):
|
||||
|
||||
self.log("joined")
|
||||
|
||||
def add2(x, y):
|
||||
self.log("add2 invoked: {0}, {1}".format(x, y))
|
||||
return x + y
|
||||
|
||||
yield self.register(add2, 'com.mathservice.add2')
|
||||
self.log("add2 registered")
|
||||
|
||||
self.finish()
|
||||
|
||||
class Case1_Frontend(CaseComponent):
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def onJoin(self, details):
|
||||
|
||||
self.log("joined")
|
||||
|
||||
try:
|
||||
res = yield self.call('com.mathservice.add2', 2, 3)
|
||||
except Exception as e:
|
||||
self.log("call error: {0}".format(e))
|
||||
else:
|
||||
self.log("call result: {0}".format(res))
|
||||
|
||||
self.finish()
|
||||
|
||||
class Case2_Backend(CaseComponent):
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def onJoin(self, details):
|
||||
|
||||
self.log("joined")
|
||||
|
||||
def ping():
|
||||
self.log("ping() is invoked")
|
||||
return
|
||||
|
||||
def add2(a, b):
|
||||
self.log("add2() is invoked", a, b)
|
||||
return a + b
|
||||
|
||||
def stars(nick="somebody", stars=0):
|
||||
self.log("stars() is invoked", nick, stars)
|
||||
return "{0} starred {1}x".format(nick, stars)
|
||||
|
||||
def orders(product, limit=5):
|
||||
self.log("orders() is invoked", product, limit)
|
||||
return ["Product {0}".format(i) for i in range(50)][:limit]
|
||||
|
||||
def arglen(*args, **kwargs):
|
||||
self.log("arglen() is invoked", args, kwargs)
|
||||
return [len(args), len(kwargs)]
|
||||
|
||||
yield self.register(ping, 'com.arguments.ping')
|
||||
yield self.register(add2, 'com.arguments.add2')
|
||||
yield self.register(stars, 'com.arguments.stars')
|
||||
yield self.register(orders, 'com.arguments.orders')
|
||||
yield self.register(arglen, 'com.arguments.arglen')
|
||||
|
||||
self.log("procedures registered")
|
||||
|
||||
class Case2_Frontend(CaseComponent):
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def onJoin(self, details):
|
||||
|
||||
self.log("joined")
|
||||
|
||||
yield sleep(1)
|
||||
|
||||
yield self.call('com.arguments.ping')
|
||||
self.log("Pinged!")
|
||||
|
||||
res = yield self.call('com.arguments.add2', 2, 3)
|
||||
self.log("Add2: {0}".format(res))
|
||||
|
||||
starred = yield self.call('com.arguments.stars')
|
||||
self.log("Starred 1: {0}".format(starred))
|
||||
|
||||
starred = yield self.call('com.arguments.stars', nick='Homer')
|
||||
self.log("Starred 2: {0}".format(starred))
|
||||
|
||||
starred = yield self.call('com.arguments.stars', stars=5)
|
||||
self.log("Starred 3: {0}".format(starred))
|
||||
|
||||
starred = yield self.call('com.arguments.stars', nick='Homer', stars=5)
|
||||
self.log("Starred 4: {0}".format(starred))
|
||||
|
||||
orders = yield self.call('com.arguments.orders', 'coffee')
|
||||
self.log("Orders 1: {0}".format(orders))
|
||||
|
||||
orders = yield self.call('com.arguments.orders', 'coffee', limit=10)
|
||||
self.log("Orders 2: {0}".format(orders))
|
||||
|
||||
arglengths = yield self.call('com.arguments.arglen')
|
||||
self.log("Arglen 1: {0}".format(arglengths))
|
||||
|
||||
arglengths = yield self.call('com.arguments.arglen', 1, 2, 3)
|
||||
self.log("Arglen 1: {0}".format(arglengths))
|
||||
|
||||
arglengths = yield self.call('com.arguments.arglen', a=1, b=2, c=3)
|
||||
self.log("Arglen 2: {0}".format(arglengths))
|
||||
|
||||
arglengths = yield self.call('com.arguments.arglen', 1, 2, 3, a=1, b=2, c=3)
|
||||
self.log("Arglen 3: {0}".format(arglengths))
|
||||
|
||||
self.log("finishing")
|
||||
|
||||
self.finish()
|
||||
|
||||
class TestRpc(unittest.TestCase):
|
||||
|
||||
if os.environ.get("WAMP_ROUTER_URL") is None:
|
||||
skip = ("Please provide WAMP_ROUTER_URL environment with url to "
|
||||
"WAMP router to run WAMP integration tests")
|
||||
|
||||
def setUp(self):
|
||||
self.url = os.environ.get("WAMP_ROUTER_URL")
|
||||
self.realm = "realm1"
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def runOneTest(self, components):
|
||||
self.deferred = defer.Deferred()
|
||||
app = service.MultiService()
|
||||
for component in components:
|
||||
c = wamp.Service(
|
||||
url=self.url,
|
||||
extra=dict(test=self),
|
||||
realm=self.realm,
|
||||
make=component,
|
||||
)
|
||||
c.setServiceParent(app)
|
||||
|
||||
app.startService()
|
||||
yield self.deferred
|
||||
app.stopService()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_case1(self):
|
||||
yield self.runOneTest([Case1_Backend, Case1_Frontend])
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_case2(self):
|
||||
yield self.runOneTest([Case2_Backend, Case2_Frontend])
|
||||
@@ -0,0 +1,154 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int 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.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest.mock as mock
|
||||
import pytest
|
||||
import txaio
|
||||
|
||||
if os.environ.get('USE_ASYNCIO', False):
|
||||
from autobahn.asyncio.component import Component
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 5), reason="requires Python 3.5+")
|
||||
@pytest.mark.asyncio(forbid_global_loop=True)
|
||||
def test_asyncio_component(event_loop):
|
||||
orig_loop = txaio.config.loop
|
||||
txaio.config.loop = event_loop
|
||||
|
||||
comp = Component(
|
||||
transports=[
|
||||
{
|
||||
"url": "ws://localhost:12/bogus",
|
||||
"max_retries": 1,
|
||||
"max_retry_delay": 0.1,
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
# if having trouble, try starting some logging (and use
|
||||
# "py.test -s" to get real-time output)
|
||||
# txaio.start_logging(level="debug")
|
||||
f = comp.start(loop=event_loop)
|
||||
txaio.config.loop = event_loop
|
||||
finished = txaio.create_future()
|
||||
|
||||
def fail():
|
||||
finished.set_exception(AssertionError("timed out"))
|
||||
txaio.config.loop = orig_loop
|
||||
txaio.call_later(4.0, fail)
|
||||
|
||||
def done(f):
|
||||
try:
|
||||
f.result()
|
||||
finished.set_exception(AssertionError("should get an error"))
|
||||
except RuntimeError as e:
|
||||
if 'Exhausted all transport connect attempts' not in str(e):
|
||||
finished.set_exception(AssertionError("wrong exception caught"))
|
||||
finished.set_result(None)
|
||||
txaio.config.loop = orig_loop
|
||||
assert comp._done_f is None
|
||||
f.add_done_callback(done)
|
||||
return finished
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 5), reason="requires Python 3.5+")
|
||||
@pytest.mark.asyncio(forbid_global_loop=True)
|
||||
def test_asyncio_component_404(event_loop):
|
||||
"""
|
||||
If something connects but then gets aborted, it should still try
|
||||
to re-connect (in real cases this could be e.g. wrong path,
|
||||
TLS failure, WebSocket handshake failure, etc)
|
||||
"""
|
||||
orig_loop = txaio.config.loop
|
||||
txaio.config.loop = event_loop
|
||||
|
||||
class FakeTransport(object):
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
def write(self, data):
|
||||
pass
|
||||
|
||||
fake_transport = FakeTransport()
|
||||
actual_protocol = [None] # set in a closure below
|
||||
|
||||
def create_connection(protocol_factory=None, server_hostname=None, host=None, port=None, ssl=False):
|
||||
if actual_protocol[0] is None:
|
||||
protocol = protocol_factory()
|
||||
actual_protocol[0] = protocol
|
||||
protocol.connection_made(fake_transport)
|
||||
return txaio.create_future_success((fake_transport, protocol))
|
||||
else:
|
||||
return txaio.create_future_error(RuntimeError("second connection fails completely"))
|
||||
|
||||
with mock.patch.object(event_loop, 'create_connection', create_connection):
|
||||
event_loop.create_connection = create_connection
|
||||
|
||||
comp = Component(
|
||||
transports=[
|
||||
{
|
||||
"url": "ws://localhost:12/bogus",
|
||||
"max_retries": 1,
|
||||
"max_retry_delay": 0.1,
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
# if having trouble, try starting some logging (and use
|
||||
# "py.test -s" to get real-time output)
|
||||
# txaio.start_logging(level="debug")
|
||||
f = comp.start(loop=event_loop)
|
||||
txaio.config.loop = event_loop
|
||||
|
||||
# now that we've started connecting, we *should* be able
|
||||
# to connetion_lost our transport .. but we do a
|
||||
# call-later to ensure we're after the setup stuff in the
|
||||
# event-loop (because asyncio doesn't synchronously
|
||||
# process already-completed Futures like Twisted does)
|
||||
|
||||
def nuke_transport():
|
||||
if actual_protocol[0] is not None:
|
||||
actual_protocol[0].connection_lost(None) # asyncio can call this with None
|
||||
txaio.call_later(0.1, nuke_transport)
|
||||
|
||||
finished = txaio.create_future()
|
||||
|
||||
def fail():
|
||||
finished.set_exception(AssertionError("timed out"))
|
||||
txaio.config.loop = orig_loop
|
||||
txaio.call_later(1.0, fail)
|
||||
|
||||
def done(f):
|
||||
try:
|
||||
f.result()
|
||||
finished.set_exception(AssertionError("should get an error"))
|
||||
except RuntimeError as e:
|
||||
if 'Exhausted all transport connect attempts' not in str(e):
|
||||
finished.set_exception(AssertionError("wrong exception caught"))
|
||||
finished.set_result(None)
|
||||
txaio.config.loop = orig_loop
|
||||
f.add_done_callback(done)
|
||||
return finished
|
||||
@@ -0,0 +1,37 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int 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 autobahn.wamp import cryptobox
|
||||
|
||||
import unittest
|
||||
|
||||
|
||||
@unittest.skipIf(not cryptobox.HAS_CRYPTOBOX, 'no cryptobox support present')
|
||||
class TestCryptoBox(unittest.TestCase):
|
||||
|
||||
def test_create_keyring(self):
|
||||
kr = cryptobox.KeyRing()
|
||||
assert kr
|
||||
@@ -0,0 +1,295 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int 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.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
import os
|
||||
import hashlib
|
||||
import binascii
|
||||
import unittest
|
||||
from unittest.mock import Mock
|
||||
|
||||
import txaio
|
||||
|
||||
if os.environ.get('USE_TWISTED', None):
|
||||
txaio.use_twisted()
|
||||
elif os.environ.get('USE_ASYNCIO', None):
|
||||
txaio.use_asyncio()
|
||||
else:
|
||||
raise RuntimeError('need either USE_TWISTED=1 or USE_ASYNCIO=1')
|
||||
|
||||
from autobahn.wamp import types
|
||||
from autobahn.wamp.auth import create_authenticator
|
||||
from autobahn.wamp.cryptosign import _makepad, HAS_CRYPTOSIGN, CryptosignAuthextra
|
||||
|
||||
if HAS_CRYPTOSIGN:
|
||||
from autobahn.wamp.cryptosign import CryptosignKey
|
||||
from nacl.encoding import HexEncoder
|
||||
|
||||
|
||||
import tempfile
|
||||
|
||||
|
||||
keybody = '''-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||
QyNTUxOQAAACAa38i/4dNWFuZN/72QAJbyOwZvkUyML/u2b2B1uW4RbQAAAJj4FLyB+BS8
|
||||
gQAAAAtzc2gtZWQyNTUxOQAAACAa38i/4dNWFuZN/72QAJbyOwZvkUyML/u2b2B1uW4RbQ
|
||||
AAAEBNV9l6aPVVaWYgpthJwM5YJWhRjXKet1PcfHMt4oBFEBrfyL/h01YW5k3/vZAAlvI7
|
||||
Bm+RTIwv+7ZvYHW5bhFtAAAAFXNvbWV1c2VyQGZ1bmt0aGF0LmNvbQ==
|
||||
-----END OPENSSH PRIVATE KEY-----'''
|
||||
|
||||
pubkey = '''ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJVp3hjHwIQyEladzd8mFcf0YSXcmyKS3qMLB7VqTQKm someuser@example.com
|
||||
'''
|
||||
|
||||
# valid test vectors for WAMP-cryptosign signature testing
|
||||
test_vectors_1 = [
|
||||
# _WITHOUT_ channel_id
|
||||
{
|
||||
'channel_id': None,
|
||||
'private_key': '4d57d97a68f555696620a6d849c0ce582568518d729eb753dc7c732de2804510',
|
||||
'challenge': 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
|
||||
'signature': 'b32675b221f08593213737bef8240e7c15228b07028e19595294678c90d11c0cae80a357331bfc5cc9fb71081464e6e75013517c2cf067ad566a6b7b728e5d03ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'
|
||||
},
|
||||
{
|
||||
'channel_id': None,
|
||||
'private_key': 'd511fe78e23934b3dadb52fcd022974b80bd92bccc7c5cf404e46cc0a8a2f5cd',
|
||||
'challenge': 'b26c1f87c13fc1da14997f1b5a71995dff8fbe0a62fae8473c7bdbd05bfb607d',
|
||||
'signature': 'd4209ad10d5aff6bfbc009d7e924795de138a63515efc7afc6b01b7fe5201372190374886a70207b042294af5bd64ce725cd8dceb344e6d11c09d1aaaf4d660fb26c1f87c13fc1da14997f1b5a71995dff8fbe0a62fae8473c7bdbd05bfb607d'
|
||||
},
|
||||
{
|
||||
'channel_id': None,
|
||||
'private_key': '6e1fde9cf9e2359a87420b65a87dc0c66136e66945196ba2475990d8a0c3a25b',
|
||||
'challenge': 'b05e6b8ad4d69abf74aa3be3c0ee40ae07d66e1895b9ab09285a2f1192d562d2',
|
||||
'signature': '7beb282184baadd08f166f16dd683b39cab53816ed81e6955def951cb2ddad1ec184e206746fd82bda075af03711d3d5658fc84a76196b0fa8d1ebc92ef9f30bb05e6b8ad4d69abf74aa3be3c0ee40ae07d66e1895b9ab09285a2f1192d562d2'
|
||||
},
|
||||
|
||||
# _WITH_ channel_id
|
||||
{
|
||||
'channel_id': '62e935ae755f3d48f80d4d59f6121358c435722a67e859cc0caa8b539027f2ff',
|
||||
'private_key': '4d57d97a68f555696620a6d849c0ce582568518d729eb753dc7c732de2804510',
|
||||
'challenge': 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
|
||||
'signature': '9b6f41540c9b95b4b7b281c3042fa9c54cef43c842d62ea3fd6030fcb66e70b3e80d49d44c29d1635da9348d02ec93f3ed1ef227dfb59a07b580095c2b82f80f9d16ca518aa0c2b707f2b2a609edeca73bca8dd59817a633f35574ac6fd80d00'
|
||||
},
|
||||
{
|
||||
'channel_id': '62e935ae755f3d48f80d4d59f6121358c435722a67e859cc0caa8b539027f2ff',
|
||||
'private_key': 'd511fe78e23934b3dadb52fcd022974b80bd92bccc7c5cf404e46cc0a8a2f5cd',
|
||||
'challenge': 'b26c1f87c13fc1da14997f1b5a71995dff8fbe0a62fae8473c7bdbd05bfb607d',
|
||||
'signature': '305aaa3ac25e98f651427688b3fc43fe7d8a68a7ec1d7d61c61517c519bd4a427c3015599d83ca28b4c652333920223844ef0725eb5dc2febfd6af7677b73f01d0852a29b460fc92ec943242ac638a053bbacc200512b18b30d15083cbdc9282'
|
||||
},
|
||||
{
|
||||
'channel_id': '62e935ae755f3d48f80d4d59f6121358c435722a67e859cc0caa8b539027f2ff',
|
||||
'private_key': '6e1fde9cf9e2359a87420b65a87dc0c66136e66945196ba2475990d8a0c3a25b',
|
||||
'challenge': 'b05e6b8ad4d69abf74aa3be3c0ee40ae07d66e1895b9ab09285a2f1192d562d2',
|
||||
'signature': 'ee3c7644fd8070532bc1fde3d70d742267da545d8c8f03e63bda63f1ad4214f4d2c4bfdb4eb9526def42deeb7e31602a6ff99eba893e0a4ad4d45892ca75e608d2b75e24a189a7f78ca776ba36fc53f6c3e31c32f251f2c524f0a44202f2902d'
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class TestSigVectors(unittest.TestCase):
|
||||
|
||||
def test_vectors(self):
|
||||
session = Mock()
|
||||
|
||||
for testvec in test_vectors_1:
|
||||
# setup fake transport details including fake channel_id
|
||||
if testvec['channel_id']:
|
||||
channel_id = binascii.a2b_hex(testvec['channel_id'])
|
||||
channel_id_type = 'tls-unique'
|
||||
session._transport.transport_details = types.TransportDetails(channel_id={'tls-unique': channel_id})
|
||||
else:
|
||||
channel_id = None
|
||||
channel_id_type = None
|
||||
session._transport.transport_details = types.TransportDetails(channel_id=None)
|
||||
|
||||
# private signing key (the seed for it)
|
||||
private_key = CryptosignKey.from_bytes(binascii.a2b_hex(testvec['private_key']))
|
||||
|
||||
# the fake challenge we've received
|
||||
challenge = types.Challenge("cryptosign", dict(challenge=testvec['challenge']))
|
||||
|
||||
# ok, now sign the challenge
|
||||
f_signed = private_key.sign_challenge(challenge,
|
||||
channel_id=channel_id,
|
||||
channel_id_type=channel_id_type)
|
||||
|
||||
def success(signed):
|
||||
# the signature returned is a Hex encoded string
|
||||
self.assertTrue(type(signed) == str)
|
||||
|
||||
# we return the concatenation of the signature and the message signed (96 bytes)
|
||||
self.assertEqual(
|
||||
192,
|
||||
len(signed),
|
||||
)
|
||||
|
||||
# must match the expected value in our test vector
|
||||
self.assertEqual(
|
||||
testvec['signature'],
|
||||
signed,
|
||||
)
|
||||
|
||||
def failed(err):
|
||||
self.fail(str(err))
|
||||
|
||||
txaio.add_callbacks(f_signed, success, failed)
|
||||
|
||||
|
||||
class TestAuth(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.key = CryptosignKey.from_ssh_bytes(keybody)
|
||||
self.privkey_hex = self.key._key.encode(encoder=HexEncoder)
|
||||
|
||||
# all tests here fake the use of channel_id_type='tls-unique' with the following channel_id
|
||||
m = hashlib.sha256()
|
||||
m.update("some TLS message".encode())
|
||||
|
||||
# 62e935ae755f3d48f80d4d59f6121358c435722a67e859cc0caa8b539027f2ff
|
||||
channel_id = m.digest()
|
||||
|
||||
self.transport_details = types.TransportDetails(channel_id={'tls-unique': channel_id})
|
||||
|
||||
def test_public_key(self):
|
||||
self.assertEqual(self.key.public_key(binary=False), '1adfc8bfe1d35616e64dffbd900096f23b066f914c8c2ffbb66f6075b96e116d')
|
||||
|
||||
def test_valid(self):
|
||||
session = Mock()
|
||||
session._transport.transport_details = self.transport_details
|
||||
|
||||
challenge = types.Challenge("cryptosign", dict(challenge="ff" * 32))
|
||||
f_signed = self.key.sign_challenge(challenge,
|
||||
channel_id=self.transport_details.channel_id['tls-unique'],
|
||||
channel_id_type='tls-unique')
|
||||
|
||||
def success(signed):
|
||||
self.assertEqual(
|
||||
192,
|
||||
len(signed),
|
||||
)
|
||||
self.assertEqual(
|
||||
'9b6f41540c9b95b4b7b281c3042fa9c54cef43c842d62ea3fd6030fcb66e70b3e80d49d44c29d1635da9348d02ec93f3ed1ef227dfb59a07b580095c2b82f80f9d16ca518aa0c2b707f2b2a609edeca73bca8dd59817a633f35574ac6fd80d00',
|
||||
signed,
|
||||
)
|
||||
|
||||
def failed(err):
|
||||
self.fail(str(err))
|
||||
|
||||
txaio.add_callbacks(f_signed, success, failed)
|
||||
|
||||
def test_authenticator(self):
|
||||
authenticator = create_authenticator(
|
||||
"cryptosign",
|
||||
authid="someone",
|
||||
authextra={'channel_binding': 'tls-unique'},
|
||||
privkey=self.privkey_hex,
|
||||
)
|
||||
session = Mock()
|
||||
session._transport.transport_details = self.transport_details
|
||||
challenge = types.Challenge("cryptosign", dict(challenge="ff" * 32))
|
||||
f_reply = authenticator.on_challenge(session, challenge)
|
||||
|
||||
def success(reply):
|
||||
self.assertEqual(
|
||||
reply,
|
||||
'9b6f41540c9b95b4b7b281c3042fa9c54cef43c842d62ea3fd6030fcb66e70b3e80d49d44c29d1635da9348d02ec93f3ed1ef227dfb59a07b580095c2b82f80f9d16ca518aa0c2b707f2b2a609edeca73bca8dd59817a633f35574ac6fd80d00',
|
||||
)
|
||||
|
||||
def failed(err):
|
||||
self.fail(str(err))
|
||||
|
||||
txaio.add_callbacks(f_reply, success, failed)
|
||||
|
||||
|
||||
class TestKey(unittest.TestCase):
|
||||
|
||||
def test_pad(self):
|
||||
self.assertEqual(_makepad(0), b'')
|
||||
self.assertEqual(_makepad(2), b'\x01\x02')
|
||||
self.assertEqual(_makepad(3), b'\x01\x02\x03')
|
||||
self.assertEqual(_makepad(30), b'\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e')
|
||||
self.assertEqual(binascii.b2a_hex(_makepad(30)).decode(), '0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e')
|
||||
|
||||
def test_key(self):
|
||||
with tempfile.NamedTemporaryFile('w+t') as fp:
|
||||
fp.write(keybody)
|
||||
fp.seek(0)
|
||||
|
||||
key = CryptosignKey.from_ssh_file(fp.name)
|
||||
self.assertEqual(key.public_key(), '1adfc8bfe1d35616e64dffbd900096f23b066f914c8c2ffbb66f6075b96e116d')
|
||||
|
||||
def test_pubkey(self):
|
||||
with tempfile.NamedTemporaryFile('w+t') as fp:
|
||||
fp.write(pubkey)
|
||||
fp.seek(0)
|
||||
|
||||
key = CryptosignKey.from_ssh_file(fp.name)
|
||||
self.assertEqual(key.public_key(binary=False), '9569de18c7c0843212569dcddf2615c7f46125dc9b2292dea30b07b56a4d02a6')
|
||||
self.assertEqual(key.comment, 'someuser@example.com')
|
||||
|
||||
|
||||
class TestAuthExtra(unittest.TestCase):
|
||||
def test_default_ctor(self):
|
||||
ae = CryptosignAuthextra()
|
||||
self.assertEqual(ae.marshal(), {})
|
||||
|
||||
def test_ctor(self):
|
||||
ae1 = CryptosignAuthextra(pubkey=b'\xff' * 32)
|
||||
self.assertEqual(ae1.marshal(), {
|
||||
'pubkey': 'ff' * 32
|
||||
})
|
||||
|
||||
ae1 = CryptosignAuthextra(pubkey=b'\xff' * 32, bandwidth=200)
|
||||
self.assertEqual(ae1.marshal(), {
|
||||
'pubkey': 'ff' * 32,
|
||||
'reservation': {
|
||||
'bandwidth': 200
|
||||
}
|
||||
})
|
||||
|
||||
def test_parse(self):
|
||||
data_original = {
|
||||
'pubkey': '9019a424b040859c108edee02e64c1dcb32b253686d7b5db56c306e9bdb2fe7e',
|
||||
'challenge': 'fe81c84e94a75a357c259d6b37361e43966a45f57dff181bb61b2f91a0f4ac88',
|
||||
'channel_binding': 'tls-unique',
|
||||
'channel_id': '2e642bf991f48ece9133a0a32d15550921dda12bfebfbc941571d4b2960540bc',
|
||||
'trustroot': '0xe78ea2fE1533D4beD9A10d91934e109A130D0ad8',
|
||||
'reservation': {
|
||||
'chain_id': 999,
|
||||
'block_no': 123456789,
|
||||
'realm': '0x163D58cE482560B7826b4612f40aa2A7d53310C4',
|
||||
'delegate': '0x72b3486d38E9f49215b487CeAaDF27D6acf22115',
|
||||
'seeder': '0x52d66f36A7927cF9612e1b40bD6549d08E0513Ff',
|
||||
'bandwidth': 200
|
||||
},
|
||||
'signature': '747763c69394270603f64af5be3f8256a14b41ff51027e583ee81db9f1f15a01cc8e55218a76139f26dbaaa78d8a537d80d248b3fc6245ecf4602cc5fbb0f6452e',
|
||||
}
|
||||
ae1 = CryptosignAuthextra.parse(data_original)
|
||||
data_marshalled = ae1.marshal()
|
||||
|
||||
# FIXME: marshal check-summed eth addresses
|
||||
data_original['trustroot'] = data_original['trustroot'].lower()
|
||||
for k in ['realm', 'delegate', 'seeder']:
|
||||
data_original['reservation'][k] = data_original['reservation'][k].lower()
|
||||
|
||||
self.assertEqual(data_marshalled, data_original)
|
||||
@@ -0,0 +1,50 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int 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 unittest import TestCase
|
||||
|
||||
from autobahn.wamp.exception import ApplicationError
|
||||
|
||||
|
||||
class ApplicationErrorTestCase(TestCase):
|
||||
|
||||
def test_unicode_str(self):
|
||||
"""
|
||||
Unicode arguments in ApplicationError will not raise an exception when
|
||||
str()'d.
|
||||
"""
|
||||
error = ApplicationError("some.url", "\u2603")
|
||||
self.assertIn("\u2603", str(error))
|
||||
|
||||
def test_unicode_errormessage(self):
|
||||
"""
|
||||
Unicode arguments in ApplicationError will not raise an exception when
|
||||
the error_message method is called.
|
||||
"""
|
||||
error = ApplicationError("some.url", "\u2603")
|
||||
# on py27-tw189: exceptions.UnicodeEncodeError: 'ascii' codec can't encode character '\u2603' in position 10: ordinal not in
|
||||
print(error.error_message())
|
||||
self.assertIn("\u2603", error.error_message())
|
||||
@@ -0,0 +1,98 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int 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 autobahn.wamp.message import check_or_raise_realm_name, identify_realm_name_category
|
||||
from autobahn.wamp.exception import InvalidUriError
|
||||
|
||||
import unittest
|
||||
|
||||
|
||||
class TestWampIdentifiers(unittest.TestCase):
|
||||
|
||||
def test_valid_realm_names(self):
|
||||
for name in [
|
||||
'realm1',
|
||||
'com.example.myapp1',
|
||||
'myapp1.example.com',
|
||||
'eth.wamp-proto',
|
||||
'wamp-proto.eth',
|
||||
'eth.wamp-proto.myapp1',
|
||||
'myapp1.wamp-proto.eth',
|
||||
'aaa',
|
||||
'Abc',
|
||||
'a00',
|
||||
'A00',
|
||||
'0x0000000000000000000000000000000000000000',
|
||||
'0xe59C7418403CF1D973485B36660728a5f4A8fF9c',
|
||||
]:
|
||||
self.assertEqual(name, check_or_raise_realm_name(name))
|
||||
|
||||
def test_invalid_realm_names(self):
|
||||
for name in [
|
||||
None,
|
||||
23,
|
||||
{},
|
||||
'',
|
||||
'.realm1',
|
||||
'123realm',
|
||||
'0x' + '00' * 64,
|
||||
'0x' + '00' * 32,
|
||||
'0x' + 'zz' * 40,
|
||||
'rlm$test',
|
||||
'a' * 256,
|
||||
]:
|
||||
self.assertRaises(InvalidUriError, check_or_raise_realm_name, name)
|
||||
|
||||
def test_realm_name_categories(self):
|
||||
for name, category in [
|
||||
# valid
|
||||
('realm1', 'standalone'),
|
||||
('com.example.myapp1', 'standalone'),
|
||||
('myapp1.example.com', 'standalone'),
|
||||
('eth.wamp-proto', 'reverse_ens'),
|
||||
('wamp-proto.eth', 'ens'),
|
||||
('eth.wamp-proto.myapp1', 'reverse_ens'),
|
||||
('myapp1.wamp-proto.eth', 'ens'),
|
||||
('aaa', 'standalone'),
|
||||
('Abc', 'standalone'),
|
||||
('a00', 'standalone'),
|
||||
('A00', 'standalone'),
|
||||
('0x0000000000000000000000000000000000000000', 'eth'),
|
||||
('0xe59C7418403CF1D973485B36660728a5f4A8fF9c', 'eth'),
|
||||
# invalid
|
||||
(None, None),
|
||||
(23, None),
|
||||
({}, None),
|
||||
('', None),
|
||||
('.realm1', None),
|
||||
('123realm', None),
|
||||
('0x' + '00' * 64, None),
|
||||
('0x' + '00' * 32, None),
|
||||
('0x' + 'zz' * 40, None),
|
||||
('rlm$test', None),
|
||||
('a' * 256, None),
|
||||
]:
|
||||
self.assertEqual(category, identify_realm_name_category(name))
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,104 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int 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.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
# we need to select a txaio subsystem because we're importing the base
|
||||
# protocol classes here for testing purposes. "normally" yo'd import
|
||||
# from autobahn.twisted.wamp or autobahn.asyncio.wamp explicitly.
|
||||
from autobahn import wamp
|
||||
from autobahn.wamp import message
|
||||
from autobahn.wamp import exception
|
||||
from autobahn.wamp import protocol
|
||||
|
||||
import unittest
|
||||
|
||||
|
||||
class TestPeerExceptions(unittest.TestCase):
|
||||
|
||||
def test_exception_from_message(self):
|
||||
session = protocol.BaseSession()
|
||||
|
||||
@wamp.error("com.myapp.error1")
|
||||
class AppError1(Exception):
|
||||
pass
|
||||
|
||||
@wamp.error("com.myapp.error2")
|
||||
class AppError2(Exception):
|
||||
pass
|
||||
|
||||
session.define(AppError1)
|
||||
session.define(AppError2)
|
||||
|
||||
# map defined errors to user exceptions
|
||||
emsg = message.Error(message.Call.MESSAGE_TYPE, 123456, 'com.myapp.error1')
|
||||
exc = session._exception_from_message(emsg)
|
||||
self.assertIsInstance(exc, AppError1)
|
||||
self.assertEqual(exc.args, ())
|
||||
|
||||
emsg = message.Error(message.Call.MESSAGE_TYPE, 123456, 'com.myapp.error2')
|
||||
exc = session._exception_from_message(emsg)
|
||||
self.assertIsInstance(exc, AppError2)
|
||||
self.assertEqual(exc.args, ())
|
||||
|
||||
# map undefined error to (generic) exception
|
||||
emsg = message.Error(message.Call.MESSAGE_TYPE, 123456, 'com.myapp.error3')
|
||||
exc = session._exception_from_message(emsg)
|
||||
self.assertIsInstance(exc, exception.ApplicationError)
|
||||
self.assertEqual(exc.error, 'com.myapp.error3')
|
||||
self.assertEqual(exc.args, ())
|
||||
self.assertEqual(exc.kwargs, {})
|
||||
|
||||
emsg = message.Error(message.Call.MESSAGE_TYPE, 123456, 'com.myapp.error3', args=[1, 2, 'hello'])
|
||||
exc = session._exception_from_message(emsg)
|
||||
self.assertIsInstance(exc, exception.ApplicationError)
|
||||
self.assertEqual(exc.error, 'com.myapp.error3')
|
||||
self.assertEqual(exc.args, (1, 2, 'hello'))
|
||||
self.assertEqual(exc.kwargs, {})
|
||||
|
||||
emsg = message.Error(message.Call.MESSAGE_TYPE, 123456, 'com.myapp.error3', args=[1, 2, 'hello'], kwargs={'foo': 23, 'bar': 'baz'})
|
||||
exc = session._exception_from_message(emsg)
|
||||
self.assertIsInstance(exc, exception.ApplicationError)
|
||||
self.assertEqual(exc.error, 'com.myapp.error3')
|
||||
self.assertEqual(exc.args, (1, 2, 'hello'))
|
||||
self.assertEqual(exc.kwargs, {'foo': 23, 'bar': 'baz'})
|
||||
|
||||
def test_message_from_exception(self):
|
||||
session = protocol.BaseSession()
|
||||
|
||||
@wamp.error("com.myapp.error1")
|
||||
class AppError1(Exception):
|
||||
pass
|
||||
|
||||
@wamp.error("com.myapp.error2")
|
||||
class AppError2(Exception):
|
||||
pass
|
||||
|
||||
session.define(AppError1)
|
||||
session.define(AppError2)
|
||||
|
||||
exc = AppError1()
|
||||
msg = session._message_from_exception(message.Call.MESSAGE_TYPE, 123456, exc)
|
||||
|
||||
self.assertEqual(msg.marshal(), [message.Error.MESSAGE_TYPE, message.Call.MESSAGE_TYPE, 123456, {}, "com.myapp.error1"])
|
||||
@@ -0,0 +1,72 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int 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.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
import unittest
|
||||
from binascii import a2b_hex
|
||||
|
||||
from autobahn.wamp.auth import derive_scram_credential
|
||||
|
||||
|
||||
TEST_VECTORS = [
|
||||
{
|
||||
'email': 'foobar@example.com',
|
||||
'password': 'secret123',
|
||||
'salt': None,
|
||||
'expected': {
|
||||
'iterations': 4096,
|
||||
'kdf': 'argon2id-13',
|
||||
'memory': 512,
|
||||
'salt': '3bc3ca01dd1d501ca1c22e1c5d7d16fe',
|
||||
'server-key': '8de7864c316f3c2356fd76cfdab696db55bc70e680fe5180e2f731e2345acca2',
|
||||
'stored-key': 'e796c2f0a51770303ee4616bc630a66774d51a55003154aff2a54ec7c4ac0e38'
|
||||
}
|
||||
},
|
||||
{
|
||||
'email': 'foobar@example.com',
|
||||
'password': 'secret123',
|
||||
'salt': a2b_hex('ae1f0d2f422757809077785e660b62c6'),
|
||||
'expected': {
|
||||
'iterations': 4096,
|
||||
'kdf': 'argon2id-13',
|
||||
'memory': 512,
|
||||
'salt': 'ae1f0d2f422757809077785e660b62c6',
|
||||
'server-key': '0d8e7e9222a7c0e54c9e979aa342115699ff5696c45dc379b5ee241338a5861d',
|
||||
'stored-key': '5f19358ff6f38e267b6ef1ea1d862514ec4e8745a84682259fd3894be09febb5'
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class TestKey(unittest.TestCase):
|
||||
|
||||
def test_derive_scram_credential(self):
|
||||
for tv in TEST_VECTORS:
|
||||
email = tv['email']
|
||||
password = tv['password']
|
||||
salt = tv['salt']
|
||||
expected = tv['expected']
|
||||
credential = derive_scram_credential(email, password, salt)
|
||||
self.assertEqual(credential, expected)
|
||||
@@ -0,0 +1,546 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int 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.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
import os
|
||||
import unittest
|
||||
import random
|
||||
import decimal
|
||||
from decimal import Decimal
|
||||
|
||||
from autobahn.wamp import message
|
||||
from autobahn.wamp import role
|
||||
from autobahn.wamp import serializer
|
||||
|
||||
|
||||
def generate_test_messages():
|
||||
"""
|
||||
List of WAMP test message used for serializers. Expand this if you add more
|
||||
options or messages.
|
||||
|
||||
This list of WAMP message does not contain any binary app payloads!
|
||||
"""
|
||||
some_bytes = os.urandom(32)
|
||||
some_unicode = '\u3053\u3093\u306b\u3061\u306f\u4e16\u754c'
|
||||
|
||||
some_uri = 'com.myapp.foobar'
|
||||
some_unicode_uri = 'com.myapp.\u4f60\u597d\u4e16\u754c.baz'
|
||||
|
||||
some_args = [1, 2, 3, 'hello', some_bytes, some_unicode, {'foo': 23, 'bar': 'hello', 'baz': some_bytes, 'moo': some_unicode}]
|
||||
some_kwargs = {'foo': 23, 'bar': 'hello', 'baz': some_bytes, 'moo': some_unicode, 'arr': some_args}
|
||||
|
||||
msgs = [
|
||||
message.Hello("realm1", {'subscriber': role.RoleSubscriberFeatures()}),
|
||||
message.Hello("realm1", {'publisher': role.RolePublisherFeatures()}),
|
||||
message.Hello("realm1", {'caller': role.RoleCallerFeatures()}),
|
||||
message.Hello("realm1", {'callee': role.RoleCalleeFeatures()}),
|
||||
message.Hello("realm1", {
|
||||
'subscriber': role.RoleSubscriberFeatures(),
|
||||
'publisher': role.RolePublisherFeatures(),
|
||||
'caller': role.RoleCallerFeatures(),
|
||||
'callee': role.RoleCalleeFeatures(),
|
||||
}),
|
||||
message.Goodbye(),
|
||||
message.Yield(123456),
|
||||
message.Yield(123456, args=some_args),
|
||||
message.Yield(123456, args=[], kwargs=some_kwargs),
|
||||
message.Yield(123456, args=some_args, kwargs=some_kwargs),
|
||||
message.Yield(123456, progress=True),
|
||||
message.Interrupt(123456),
|
||||
message.Interrupt(123456, mode=message.Interrupt.KILL),
|
||||
message.Invocation(123456, 789123),
|
||||
message.Invocation(123456, 789123, args=some_args),
|
||||
message.Invocation(123456, 789123, args=[], kwargs=some_kwargs),
|
||||
message.Invocation(123456, 789123, args=some_args, kwargs=some_kwargs),
|
||||
message.Invocation(123456, 789123, timeout=10000),
|
||||
message.Result(123456),
|
||||
message.Result(123456, args=some_args),
|
||||
message.Result(123456, args=[], kwargs=some_kwargs),
|
||||
message.Result(123456, args=some_args, kwargs=some_kwargs),
|
||||
message.Result(123456, progress=True),
|
||||
message.Cancel(123456),
|
||||
message.Cancel(123456, mode=message.Cancel.KILL),
|
||||
message.Call(123456, some_uri),
|
||||
message.Call(123456, some_uri, args=some_args),
|
||||
message.Call(123456, some_uri, args=[], kwargs=some_kwargs),
|
||||
message.Call(123456, some_uri, args=some_args, kwargs=some_kwargs),
|
||||
message.Call(123456, some_uri, timeout=10000),
|
||||
message.Call(123456, some_unicode_uri),
|
||||
message.Call(123456, some_unicode_uri, args=some_args),
|
||||
message.Call(123456, some_unicode_uri, args=[], kwargs=some_kwargs),
|
||||
message.Call(123456, some_unicode_uri, args=some_args, kwargs=some_kwargs),
|
||||
message.Call(123456, some_unicode_uri, timeout=10000),
|
||||
message.Unregistered(123456),
|
||||
message.Unregister(123456, 789123),
|
||||
message.Registered(123456, 789123),
|
||||
message.Register(123456, some_uri),
|
||||
message.Register(123456, some_uri, match='prefix'),
|
||||
message.Register(123456, some_uri, invoke='roundrobin'),
|
||||
message.Register(123456, some_unicode_uri),
|
||||
message.Register(123456, some_unicode_uri, match='prefix'),
|
||||
message.Register(123456, some_unicode_uri, invoke='roundrobin'),
|
||||
message.Event(123456, 789123),
|
||||
message.Event(123456, 789123, args=some_args),
|
||||
message.Event(123456, 789123, args=[], kwargs=some_kwargs),
|
||||
message.Event(123456, 789123, args=some_args, kwargs=some_kwargs),
|
||||
message.Event(123456, 789123, publisher=300),
|
||||
message.Published(123456, 789123),
|
||||
message.Publish(123456, some_uri),
|
||||
message.Publish(123456, some_uri, args=some_args),
|
||||
message.Publish(123456, some_uri, args=[], kwargs=some_kwargs),
|
||||
message.Publish(123456, some_uri, args=some_args, kwargs=some_kwargs),
|
||||
message.Publish(123456, some_uri, exclude_me=False, exclude=[300], eligible=[100, 200, 300]),
|
||||
message.Publish(123456, some_unicode_uri),
|
||||
message.Publish(123456, some_unicode_uri, args=some_args),
|
||||
message.Publish(123456, some_unicode_uri, args=[], kwargs=some_kwargs),
|
||||
message.Publish(123456, some_unicode_uri, args=some_args, kwargs=some_kwargs),
|
||||
message.Publish(123456, some_unicode_uri, exclude_me=False, exclude=[300], eligible=[100, 200, 300]),
|
||||
message.Unsubscribed(123456),
|
||||
message.Unsubscribe(123456, 789123),
|
||||
message.Subscribed(123456, 789123),
|
||||
message.Subscribe(123456, some_uri),
|
||||
message.Subscribe(123456, some_uri, match=message.Subscribe.MATCH_PREFIX),
|
||||
message.Subscribe(123456, some_unicode_uri),
|
||||
message.Subscribe(123456, some_unicode_uri, match=message.Subscribe.MATCH_PREFIX),
|
||||
message.Error(message.Call.MESSAGE_TYPE, 123456, some_uri),
|
||||
message.Error(message.Call.MESSAGE_TYPE, 123456, some_uri, args=some_args),
|
||||
message.Error(message.Call.MESSAGE_TYPE, 123456, some_uri, args=[], kwargs=some_kwargs),
|
||||
message.Error(message.Call.MESSAGE_TYPE, 123456, some_uri, args=some_args, kwargs=some_kwargs),
|
||||
message.Error(message.Call.MESSAGE_TYPE, 123456, some_unicode_uri),
|
||||
message.Error(message.Call.MESSAGE_TYPE, 123456, some_unicode_uri, args=some_args),
|
||||
message.Error(message.Call.MESSAGE_TYPE, 123456, some_unicode_uri, args=[], kwargs=some_kwargs),
|
||||
message.Error(message.Call.MESSAGE_TYPE, 123456, some_unicode_uri, args=some_args, kwargs=some_kwargs),
|
||||
message.Result(123456),
|
||||
message.Result(123456, args=some_args),
|
||||
message.Result(123456, args=some_args, kwargs=some_kwargs),
|
||||
]
|
||||
return [(False, msg) for msg in msgs]
|
||||
|
||||
|
||||
def generate_test_messages_binary():
|
||||
"""
|
||||
Generate WAMP test messages which contain binary app payloads.
|
||||
|
||||
With the JSON serializer, this currently only works on Python 3 (both CPython3 and PyPy3),
|
||||
because even on Python 3, we need to patch the stdlib JSON, and on Python 2, the patching
|
||||
would be even hackier.
|
||||
"""
|
||||
msgs = []
|
||||
for binary in [b'',
|
||||
b'\x00',
|
||||
b'\30',
|
||||
os.urandom(4),
|
||||
os.urandom(16),
|
||||
os.urandom(128),
|
||||
os.urandom(256),
|
||||
os.urandom(512),
|
||||
os.urandom(1024)]:
|
||||
msgs.append(message.Event(123456, 789123, args=[binary]))
|
||||
msgs.append(message.Event(123456, 789123, args=[binary], kwargs={'foo': binary}))
|
||||
return [(True, msg) for msg in msgs]
|
||||
|
||||
|
||||
def create_serializers(decimal_support=False):
|
||||
_serializers = []
|
||||
|
||||
_serializers.append(serializer.JsonSerializer(use_decimal_from_str=decimal_support))
|
||||
_serializers.append(serializer.JsonSerializer(batched=True, use_decimal_from_str=decimal_support))
|
||||
|
||||
_serializers.append(serializer.CBORSerializer())
|
||||
_serializers.append(serializer.CBORSerializer(batched=True))
|
||||
|
||||
if not decimal_support:
|
||||
# builtins.OverflowError: Integer value out of range
|
||||
_serializers.append(serializer.MsgPackSerializer())
|
||||
_serializers.append(serializer.MsgPackSerializer(batched=True))
|
||||
|
||||
# roundtrip error
|
||||
_serializers.append(serializer.UBJSONSerializer())
|
||||
_serializers.append(serializer.UBJSONSerializer(batched=True))
|
||||
|
||||
# FIXME: implement full FlatBuffers serializer for WAMP
|
||||
# WAMP-FlatBuffers currently only supports Python 3
|
||||
# _serializers.append(serializer.FlatBuffersSerializer())
|
||||
# _serializers.append(serializer.FlatBuffersSerializer(batched=True))
|
||||
|
||||
return _serializers
|
||||
|
||||
|
||||
class TestFlatBuffersSerializer(unittest.TestCase):
|
||||
|
||||
def test_basic(self):
|
||||
messages = [
|
||||
message.Event(123456,
|
||||
789123,
|
||||
args=[1, 2, 3],
|
||||
kwargs={'foo': 23, 'bar': 'hello'},
|
||||
publisher=666,
|
||||
retained=True),
|
||||
message.Publish(123456,
|
||||
'com.example.topic1',
|
||||
args=[1, 2, 3],
|
||||
kwargs={'foo': 23, 'bar': 'hello'},
|
||||
retain=True)
|
||||
]
|
||||
|
||||
ser = serializer.FlatBuffersSerializer()
|
||||
|
||||
# from pprint import pprint
|
||||
|
||||
for msg in messages:
|
||||
|
||||
# serialize message
|
||||
payload, binary = ser.serialize(msg)
|
||||
|
||||
# unserialize message again
|
||||
msg2 = ser.unserialize(payload, binary)[0]
|
||||
|
||||
# pprint(msg.marshal())
|
||||
# pprint(msg2.marshal())
|
||||
|
||||
# must be equal: message roundtrips via the serializer
|
||||
self.assertEqual(msg, msg2)
|
||||
# self.assertEqual(msg.subscription, msg2.subscription)
|
||||
# self.assertEqual(msg.publication, msg2.publication)
|
||||
|
||||
|
||||
class TestDecimalSerializer(unittest.TestCase):
|
||||
"""
|
||||
binary fixed-point
|
||||
binary floating-point: float (float32), double (float64)
|
||||
decimal floating-point: decimal128, decimal256
|
||||
decimal fixed-point: NUMERIC(precision, scale)
|
||||
decimal arbitrary precision: NUMERIC, decimal.Decimal
|
||||
|
||||
https://developer.nvidia.com/blog/implementing-high-precision-decimal-arithmetic-with-cuda-int128/
|
||||
https://github.com/johnmcfarlane/cnl
|
||||
"""
|
||||
def setUp(self) -> None:
|
||||
self._test_serializers = create_serializers(decimal_support=True)
|
||||
|
||||
# enough for decimal256 precision arithmetic (76 significand decimal digits)
|
||||
decimal.getcontext().prec = 76
|
||||
|
||||
self._test_messages_no_dec = [
|
||||
(True,
|
||||
{
|
||||
'a': random.random(),
|
||||
'b': random.randint(0, 2 ** 53),
|
||||
'c': random.randint(0, 2 ** 64),
|
||||
'd': random.randint(0, 2 ** 128),
|
||||
'e': random.randint(0, 2 ** 256),
|
||||
# float64: 52 binary digits, precision of 15-17 significant decimal digits
|
||||
'f': 0.12345678901234567,
|
||||
'g': 0.8765432109876545,
|
||||
'y': os.urandom(8),
|
||||
'z': [
|
||||
-1,
|
||||
0,
|
||||
1,
|
||||
True,
|
||||
None,
|
||||
0.12345678901234567,
|
||||
0.8765432109876545,
|
||||
os.urandom(8)
|
||||
]
|
||||
})
|
||||
]
|
||||
self._test_messages_dec = [
|
||||
(True,
|
||||
{
|
||||
'a': random.random(),
|
||||
'b': random.randint(0, 2 ** 53),
|
||||
'c': random.randint(0, 2 ** 64),
|
||||
'd': random.randint(0, 2 ** 128),
|
||||
'e': random.randint(0, 2 ** 256),
|
||||
# float64: 52 binary digits, precision of 15-17 significant decimal digits
|
||||
'f': 0.12345678901234567,
|
||||
'g': 0.8765432109876545,
|
||||
# decimal128: precision of 38 significant decimal digits
|
||||
'h': Decimal('0.1234567890123456789012345678901234567'),
|
||||
'i': Decimal('0.8765432109876543210987654321098765434'),
|
||||
# decimal256: precision of 76 significant decimal digits
|
||||
'j': Decimal('0.123456789012345678901234567890123456701234567890123456789012345678901234567'),
|
||||
'k': Decimal('0.876543210987654321098765432109876543298765432109876543210987654321098765434'),
|
||||
'y': os.urandom(8),
|
||||
'z': [
|
||||
-1,
|
||||
0,
|
||||
1,
|
||||
True,
|
||||
None,
|
||||
0.12345678901234567,
|
||||
0.8765432109876545,
|
||||
Decimal('0.1234567890123456789012345678901234567'),
|
||||
Decimal('0.8765432109876543210987654321098765434'),
|
||||
Decimal('0.123456789012345678901234567890123456701234567890123456789012345678901234567'),
|
||||
Decimal('0.876543210987654321098765432109876543298765432109876543210987654321098765434'),
|
||||
os.urandom(8)
|
||||
]
|
||||
})
|
||||
]
|
||||
|
||||
def test_json_no_decimal(self):
|
||||
"""
|
||||
Test without ``use_decimal_from_str`` feature of JSON object serializer.
|
||||
"""
|
||||
ser = serializer.JsonObjectSerializer(use_decimal_from_str=False)
|
||||
for contains_binary, obj in self._test_messages_no_dec:
|
||||
_obj = ser.unserialize(ser.serialize(obj))[0]
|
||||
self.assertEqual(obj, _obj)
|
||||
self.assertEqual(1.0000000000000002, _obj['f'] + _obj['g'])
|
||||
|
||||
def test_json_decimal(self):
|
||||
"""
|
||||
Test ``use_decimal_from_str`` feature of JSON object serializer.
|
||||
"""
|
||||
ser = serializer.JsonObjectSerializer(use_decimal_from_str=True)
|
||||
for contains_binary, obj in self._test_messages_dec:
|
||||
_obj = ser.unserialize(ser.serialize(obj))[0]
|
||||
self.assertEqual(obj, _obj)
|
||||
self.assertEqual(1.0000000000000002, _obj['f'] + _obj['g'])
|
||||
self.assertEqual(Decimal('1.0000000000000000000000000000000000001'), _obj['h'] + _obj['i'])
|
||||
self.assertEqual(Decimal('1.000000000000000000000000000000000000000000000000000000000000000000000000001'), _obj['j'] + _obj['k'])
|
||||
|
||||
def test_roundtrip_msg(self):
|
||||
for wamp_ser in self._test_serializers:
|
||||
ser = wamp_ser._serializer
|
||||
for contains_binary, msg in self._test_messages_no_dec + self._test_messages_dec:
|
||||
payload = ser.serialize(msg)
|
||||
msg2 = ser.unserialize(payload)
|
||||
self.assertEqual(msg, msg2[0])
|
||||
|
||||
def test_crosstrip_msg(self):
|
||||
for wamp_ser1 in self._test_serializers:
|
||||
ser1 = wamp_ser1._serializer
|
||||
for contains_binary, msg in self._test_messages_no_dec + self._test_messages_dec:
|
||||
payload1 = ser1.serialize(msg)
|
||||
msg1 = ser1.unserialize(payload1)
|
||||
msg1 = msg1[0]
|
||||
for wamp_ser2 in self._test_serializers:
|
||||
ser2 = wamp_ser2._serializer
|
||||
payload2 = ser2.serialize(msg1)
|
||||
msg2 = ser2.unserialize(payload2)
|
||||
msg2 = msg2[0]
|
||||
self.assertEqual(msg, msg2)
|
||||
# print(ser1, len(payload1), ser2, len(payload2))
|
||||
|
||||
|
||||
class TestSerializer(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self._test_messages = generate_test_messages() + generate_test_messages_binary()
|
||||
self._test_serializers = create_serializers()
|
||||
# print('Testing WAMP serializers {} with {} WAMP test messages'.format([ser.SERIALIZER_ID for ser in self._test_serializers], len(self._test_messages)))
|
||||
|
||||
def test_deep_equal_msg(self):
|
||||
"""
|
||||
Test deep object equality assert (because I am paranoid).
|
||||
"""
|
||||
v = os.urandom(10)
|
||||
o1 = [1, 2, {'foo': 'bar', 'bar': v, 'baz': [9, 3, 2], 'goo': {'moo': [1, 2, 3]}}, v]
|
||||
o2 = [1, 2, {'goo': {'moo': [1, 2, 3]}, 'bar': v, 'baz': [9, 3, 2], 'foo': 'bar'}, v]
|
||||
self.assertEqual(o1, o2)
|
||||
|
||||
def test_roundtrip_msg(self):
|
||||
"""
|
||||
Test round-tripping over each serializer.
|
||||
"""
|
||||
for ser in self._test_serializers:
|
||||
|
||||
for contains_binary, msg in self._test_messages:
|
||||
|
||||
# serialize message
|
||||
payload, binary = ser.serialize(msg)
|
||||
|
||||
# unserialize message again
|
||||
msg2 = ser.unserialize(payload, binary)
|
||||
|
||||
# must be equal: message roundtrips via the serializer
|
||||
self.assertEqual([msg], msg2)
|
||||
|
||||
def test_crosstrip_msg(self):
|
||||
"""
|
||||
Test cross-tripping over 2 serializers (as is done by WAMP routers).
|
||||
"""
|
||||
for ser1 in self._test_serializers:
|
||||
|
||||
for contains_binary, msg in self._test_messages:
|
||||
|
||||
# serialize message
|
||||
payload, binary = ser1.serialize(msg)
|
||||
|
||||
# unserialize message again
|
||||
msg1 = ser1.unserialize(payload, binary)
|
||||
msg1 = msg1[0]
|
||||
|
||||
for ser2 in self._test_serializers:
|
||||
|
||||
# serialize message
|
||||
payload, binary = ser2.serialize(msg1)
|
||||
|
||||
# unserialize message again
|
||||
msg2 = ser2.unserialize(payload, binary)
|
||||
|
||||
# must be equal: message crosstrips via
|
||||
# the serializers ser1 -> ser2
|
||||
self.assertEqual([msg], msg2)
|
||||
|
||||
def test_cache_msg(self):
|
||||
"""
|
||||
Test message serialization caching.
|
||||
"""
|
||||
for contains_binary, msg in self._test_messages:
|
||||
|
||||
# message serialization cache is initially empty
|
||||
self.assertEqual(msg._serialized, {})
|
||||
|
||||
for ser in self._test_serializers:
|
||||
|
||||
# verify message serialization is not yet cached
|
||||
self.assertFalse(ser._serializer in msg._serialized)
|
||||
payload, binary = ser.serialize(msg)
|
||||
|
||||
# now the message serialization must be cached
|
||||
self.assertTrue(ser._serializer in msg._serialized)
|
||||
self.assertEqual(msg._serialized[ser._serializer], payload)
|
||||
|
||||
# and after resetting the serialization cache, message
|
||||
# serialization is gone
|
||||
msg.uncache()
|
||||
self.assertFalse(ser._serializer in msg._serialized)
|
||||
|
||||
def test_initial_stats(self):
|
||||
"""
|
||||
Test initial serializer stats are indeed empty.
|
||||
"""
|
||||
for ser in self._test_serializers:
|
||||
|
||||
stats = ser.stats(details=True)
|
||||
|
||||
self.assertEqual(stats['serialized']['bytes'], 0)
|
||||
self.assertEqual(stats['serialized']['messages'], 0)
|
||||
self.assertEqual(stats['serialized']['rated_messages'], 0)
|
||||
|
||||
self.assertEqual(stats['unserialized']['bytes'], 0)
|
||||
self.assertEqual(stats['unserialized']['messages'], 0)
|
||||
self.assertEqual(stats['unserialized']['rated_messages'], 0)
|
||||
|
||||
def test_serialize_stats(self):
|
||||
"""
|
||||
Test serializer stats are non-empty after serializing/unserializing messages.
|
||||
"""
|
||||
for ser in self._test_serializers:
|
||||
|
||||
for contains_binary, msg in self._test_messages:
|
||||
|
||||
# serialize message
|
||||
payload, binary = ser.serialize(msg)
|
||||
|
||||
# unserialize message again
|
||||
ser.unserialize(payload, binary)
|
||||
|
||||
stats = ser.stats(details=False)
|
||||
|
||||
self.assertTrue(stats['bytes'] > 0)
|
||||
self.assertTrue(stats['messages'] > 0)
|
||||
self.assertTrue(stats['rated_messages'] > 0)
|
||||
|
||||
def test_serialize_stats_with_details(self):
|
||||
"""
|
||||
Test serializer stats - with details - are non-empty after serializing/unserializing messages.
|
||||
"""
|
||||
for ser in self._test_serializers:
|
||||
|
||||
for contains_binary, msg in self._test_messages:
|
||||
|
||||
# serialize message
|
||||
payload, binary = ser.serialize(msg)
|
||||
|
||||
# unserialize message again
|
||||
ser.unserialize(payload, binary)
|
||||
|
||||
stats = ser.stats(details=True)
|
||||
|
||||
# {'serialized': {'bytes': 7923, 'messages': 59, 'rated_messages': 69}, 'unserialized': {'bytes': 7923, 'messages': 59, 'rated_messages': 69}}
|
||||
# print(stats)
|
||||
|
||||
self.assertTrue(stats['serialized']['bytes'] > 0)
|
||||
self.assertTrue(stats['serialized']['messages'] > 0)
|
||||
self.assertTrue(stats['serialized']['rated_messages'] > 0)
|
||||
|
||||
self.assertTrue(stats['unserialized']['bytes'] > 0)
|
||||
self.assertTrue(stats['unserialized']['messages'] > 0)
|
||||
self.assertTrue(stats['unserialized']['rated_messages'] > 0)
|
||||
|
||||
self.assertEqual(stats['serialized']['bytes'], stats['unserialized']['bytes'])
|
||||
self.assertEqual(stats['serialized']['messages'], stats['unserialized']['messages'])
|
||||
self.assertEqual(stats['serialized']['rated_messages'], stats['unserialized']['rated_messages'])
|
||||
|
||||
def test_reset_stats(self):
|
||||
"""
|
||||
Test serializer stats are reset after fetching stats - depending on option.
|
||||
"""
|
||||
for ser in self._test_serializers:
|
||||
|
||||
for contains_binary, msg in self._test_messages:
|
||||
|
||||
# serialize message
|
||||
payload, binary = ser.serialize(msg)
|
||||
|
||||
# unserialize message again
|
||||
ser.unserialize(payload, binary)
|
||||
|
||||
ser.stats()
|
||||
stats = ser.stats(details=True)
|
||||
|
||||
self.assertEqual(stats['serialized']['bytes'], 0)
|
||||
self.assertEqual(stats['serialized']['messages'], 0)
|
||||
self.assertEqual(stats['serialized']['rated_messages'], 0)
|
||||
|
||||
self.assertEqual(stats['unserialized']['bytes'], 0)
|
||||
self.assertEqual(stats['unserialized']['messages'], 0)
|
||||
self.assertEqual(stats['unserialized']['rated_messages'], 0)
|
||||
|
||||
def test_auto_stats(self):
|
||||
"""
|
||||
Test serializer stats are non-empty after serializing/unserializing messages.
|
||||
"""
|
||||
for ser in self._test_serializers:
|
||||
|
||||
def on_stats(stats):
|
||||
self.assertTrue(stats['bytes'] > 0)
|
||||
self.assertTrue(stats['messages'] > 0)
|
||||
self.assertTrue(stats['rated_messages'] > 0)
|
||||
|
||||
ser.set_stats_autoreset(10, 0, on_stats)
|
||||
|
||||
for contains_binary, msg in self._test_messages:
|
||||
|
||||
# serialize message
|
||||
payload, binary = ser.serialize(msg)
|
||||
|
||||
# unserialize message again
|
||||
ser.unserialize(payload, binary)
|
||||
@@ -0,0 +1,88 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int 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 autobahn.wamp.types import TransportDetails, SessionDetails
|
||||
|
||||
import unittest
|
||||
|
||||
# from .test_wamp_transport_details import TRANSPORT_DETAILS_1
|
||||
from autobahn.wamp.test.test_wamp_transport_details import TRANSPORT_DETAILS_1
|
||||
|
||||
|
||||
class TestSessionDetails(unittest.TestCase):
|
||||
|
||||
def test_empty(self):
|
||||
sd1 = SessionDetails()
|
||||
data = sd1.marshal()
|
||||
self.assertEqual(data, {
|
||||
'realm': None,
|
||||
'session': None,
|
||||
'authid': None,
|
||||
'authrole': None,
|
||||
'authmethod': None,
|
||||
'authprovider': None,
|
||||
'authextra': None,
|
||||
'serializer': None,
|
||||
'transport': None,
|
||||
'resumed': None,
|
||||
'resumable': None,
|
||||
'resume_token': None,
|
||||
})
|
||||
sd2 = SessionDetails.parse(data)
|
||||
self.assertEqual(sd2, sd1)
|
||||
|
||||
def test_attributes(self):
|
||||
sd1 = SessionDetails()
|
||||
td1 = TransportDetails.parse(TRANSPORT_DETAILS_1)
|
||||
sd1.realm = 'realm1'
|
||||
sd1.session = 666
|
||||
sd1.authid = 'homer'
|
||||
sd1.authrole = 'user'
|
||||
sd1.authmethod = 'wampcra'
|
||||
sd1.authprovider = 'static'
|
||||
sd1.authextra = {'foo': 'bar', 'baz': [1, 2, 3]}
|
||||
sd1.serializer = 'json'
|
||||
sd1.transport = td1
|
||||
sd1.resumed = False
|
||||
sd1.resumable = True
|
||||
sd1.resume_token = '8713e25a-d4f5-48b7-9d6d-eda66603a1ab'
|
||||
data = sd1.marshal()
|
||||
self.assertEqual(data, {
|
||||
'realm': sd1.realm,
|
||||
'session': sd1.session,
|
||||
'authid': sd1.authid,
|
||||
'authrole': sd1.authrole,
|
||||
'authmethod': sd1.authmethod,
|
||||
'authprovider': sd1.authprovider,
|
||||
'authextra': sd1.authextra,
|
||||
'serializer': sd1.serializer,
|
||||
'transport': sd1.transport.marshal(),
|
||||
'resumed': sd1.resumed,
|
||||
'resumable': sd1.resumable,
|
||||
'resume_token': sd1.resume_token,
|
||||
})
|
||||
sd2 = SessionDetails.parse(data)
|
||||
self.assertEqual(sd2, sd1)
|
||||
@@ -0,0 +1,138 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int 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 autobahn.wamp.types import TransportDetails
|
||||
|
||||
import unittest
|
||||
|
||||
TRANSPORT_DETAILS_1 = {
|
||||
# TransportDetails.CHANNEL_TYPE_TO_STR[TransportDetails.CHANNEL_TYPE_TCP]
|
||||
'channel_type': 'tcp',
|
||||
|
||||
# TransportDetails.CHANNEL_FRAMING_TO_STR[TransportDetails.CHANNEL_FRAMING_WEBSOCKET]
|
||||
'channel_framing': 'websocket',
|
||||
|
||||
# TransportDetails.CHANNEL_SERIALIZER_TO_STR[TransportDetails.CHANNEL_SERIALIZER_CBOR]
|
||||
'channel_serializer': 'cbor',
|
||||
|
||||
# This end of the connection
|
||||
'own': 'ws://localhost:8080/ws',
|
||||
'own_pid': 9182731,
|
||||
'own_tid': 7563483,
|
||||
'own_fd': 20914571,
|
||||
|
||||
# Peer of the connection
|
||||
'peer': 'tcp4:127.0.0.1:48576',
|
||||
'is_server': True,
|
||||
|
||||
# TLS
|
||||
'is_secure': False,
|
||||
'channel_id': None,
|
||||
'peer_cert': None,
|
||||
|
||||
# only filled when using WebSocket
|
||||
'websocket_protocol': 'wamp.2.cbor.batched',
|
||||
'websocket_extensions_in_use': None,
|
||||
|
||||
# only filled when using HTTP (including regular WebSocket)
|
||||
'http_headers_received': {'cache-control': 'no-cache',
|
||||
'connection': 'Upgrade',
|
||||
'host': 'localhost:8080',
|
||||
'pragma': 'no-cache',
|
||||
'sec-websocket-extensions': 'permessage-deflate; '
|
||||
'client_no_context_takeover; '
|
||||
'client_max_window_bits',
|
||||
'sec-websocket-key': 'Q+t++aGQJPaFLzDW7LktEQ==',
|
||||
'sec-websocket-protocol': 'wamp.2.cbor.batched,wamp.2.cbor,wamp.2.msgpack.batched,wamp.2.msgpack,wamp.2.ubjson.batched,wamp.2.ubjson,wamp.2.json.batched,wamp.2.json',
|
||||
'sec-websocket-version': '13',
|
||||
'upgrade': 'WebSocket',
|
||||
'user-agent': 'AutobahnPython/22.4.1.dev5'},
|
||||
'http_headers_sent': {'Set-Cookie': 'cbtid=JD27oZC18xS+O4VE9+x5iyKR;max-age=604800'},
|
||||
'http_cbtid': 'JD27oZC18xS+O4VE9+x5iyKR',
|
||||
}
|
||||
|
||||
|
||||
class TestTransportDetails(unittest.TestCase):
|
||||
|
||||
def test_ctor_empty(self):
|
||||
td = TransportDetails()
|
||||
data = td.marshal()
|
||||
self.assertEqual(data, {
|
||||
'channel_type': None,
|
||||
'channel_framing': None,
|
||||
'channel_serializer': None,
|
||||
'own': None,
|
||||
'peer': None,
|
||||
'is_server': None,
|
||||
'own_pid': None,
|
||||
'own_tid': None,
|
||||
'own_fd': None,
|
||||
'is_secure': None,
|
||||
'channel_id': None,
|
||||
'peer_cert': None,
|
||||
'websocket_protocol': None,
|
||||
'websocket_extensions_in_use': None,
|
||||
'http_headers_received': None,
|
||||
'http_headers_sent': None,
|
||||
'http_cbtid': None,
|
||||
})
|
||||
td2 = TransportDetails.parse(td.marshal())
|
||||
self.assertEqual(td2, td)
|
||||
|
||||
def test_attributes(self):
|
||||
td = TransportDetails()
|
||||
for channel_type in TransportDetails.CHANNEL_TYPE_TO_STR:
|
||||
td.channel_type = channel_type
|
||||
self.assertEqual(td.channel_type, channel_type)
|
||||
|
||||
for channel_framing in TransportDetails.CHANNEL_FRAMING_TO_STR:
|
||||
td.channel_framing = channel_framing
|
||||
self.assertEqual(td.channel_framing, channel_framing)
|
||||
|
||||
for channel_serializer in TransportDetails.CHANNEL_SERIALIZER_TO_STR:
|
||||
td.channel_serializer = channel_serializer
|
||||
self.assertEqual(td.channel_serializer, channel_serializer)
|
||||
|
||||
def test_parse(self):
|
||||
td = TransportDetails.parse(TRANSPORT_DETAILS_1)
|
||||
data2 = td.marshal()
|
||||
self.maxDiff = None
|
||||
self.assertEqual(data2, TRANSPORT_DETAILS_1)
|
||||
|
||||
def test_channel_typeid(self):
|
||||
# test empty
|
||||
td = TransportDetails()
|
||||
self.assertEqual(td.channel_typeid, 'null-null-null')
|
||||
|
||||
# test all combinations
|
||||
for channel_type in TransportDetails.CHANNEL_TYPE_TO_STR:
|
||||
for channel_framing in TransportDetails.CHANNEL_FRAMING_TO_STR:
|
||||
for channel_serializer in TransportDetails.CHANNEL_SERIALIZER_TO_STR:
|
||||
td = TransportDetails(channel_type=channel_type, channel_framing=channel_framing, channel_serializer=channel_serializer)
|
||||
channel_typeid = '{}-{}-{}'.format(TransportDetails.CHANNEL_TYPE_TO_STR[channel_type],
|
||||
TransportDetails.CHANNEL_FRAMING_TO_STR[channel_framing],
|
||||
TransportDetails.CHANNEL_SERIALIZER_TO_STR[channel_serializer])
|
||||
self.assertEqual(td.channel_typeid, channel_typeid)
|
||||
@@ -0,0 +1,573 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int 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 autobahn import wamp
|
||||
from autobahn.wamp.uri import Pattern, RegisterOptions, SubscribeOptions
|
||||
|
||||
import unittest
|
||||
|
||||
|
||||
class TestUris(unittest.TestCase):
|
||||
|
||||
def test_invalid_uris(self):
|
||||
for u in ["",
|
||||
"com.myapp.<product:foo>.update",
|
||||
"com.myapp.<123:int>.update",
|
||||
"com.myapp.<:product>.update",
|
||||
"com.myapp.<product:>.update",
|
||||
"com.myapp.<int:>.update",
|
||||
]:
|
||||
self.assertRaises(Exception, Pattern, u, Pattern.URI_TARGET_ENDPOINT)
|
||||
|
||||
def test_valid_uris(self):
|
||||
for u in ["com.myapp.proc1",
|
||||
"123",
|
||||
"com.myapp.<product:int>.update",
|
||||
"com.myapp.<category:string>.<subcategory>.list"
|
||||
"com.myapp.something..update"
|
||||
]:
|
||||
p = Pattern(u, Pattern.URI_TARGET_ENDPOINT)
|
||||
self.assertIsInstance(p, Pattern)
|
||||
|
||||
def test_parse_uris(self):
|
||||
tests = [
|
||||
("com.myapp.<product:int>.update", [
|
||||
("com.myapp.0.update", {'product': 0}),
|
||||
("com.myapp.123456.update", {'product': 123456}),
|
||||
("com.myapp.aaa.update", None),
|
||||
("com.myapp..update", None),
|
||||
("com.myapp.0.delete", None),
|
||||
]),
|
||||
("com.myapp.<product:string>.update", [
|
||||
("com.myapp.box.update", {'product': 'box'}),
|
||||
("com.myapp.123456.update", {'product': '123456'}),
|
||||
("com.myapp..update", None),
|
||||
]),
|
||||
("com.myapp.<product>.update", [
|
||||
("com.myapp.0.update", {'product': '0'}),
|
||||
("com.myapp.abc.update", {'product': 'abc'}),
|
||||
("com.myapp..update", None),
|
||||
]),
|
||||
("com.myapp.<category:string>.<subcategory:string>.list", [
|
||||
("com.myapp.cosmetic.shampoo.list", {'category': 'cosmetic', 'subcategory': 'shampoo'}),
|
||||
("com.myapp...list", None),
|
||||
("com.myapp.cosmetic..list", None),
|
||||
("com.myapp..shampoo.list", None),
|
||||
]),
|
||||
("eth.pydefi.tradeclock.<clock_oid:str>.get_clock_info", [
|
||||
("eth.pydefi.tradeclock.ba3b1e9f-3006-4eae-ae88-cf5896b36342.get_clock_info",
|
||||
{"clock_oid": "ba3b1e9f-3006-4eae-ae88-cf5896b36342"}),
|
||||
]),
|
||||
("eth.wamp.network.catalog.<catalog_adr:str>.owner", [
|
||||
("eth.wamp.network.catalog.0xAA8Cc377db31a354137d8Bb86D0E38495dbD5266.owner",
|
||||
{"catalog_adr": "0xAA8Cc377db31a354137d8Bb86D0E38495dbD5266"}),
|
||||
]),
|
||||
]
|
||||
for test in tests:
|
||||
pat = Pattern(test[0], Pattern.URI_TARGET_ENDPOINT)
|
||||
for ptest in test[1]:
|
||||
uri = ptest[0]
|
||||
kwargs_should = ptest[1]
|
||||
if kwargs_should is not None:
|
||||
args_is, kwargs_is = pat.match(uri)
|
||||
self.assertEqual(kwargs_is, kwargs_should)
|
||||
else:
|
||||
self.assertRaises(Exception, pat.match, uri)
|
||||
|
||||
|
||||
class TestDecorators(unittest.TestCase):
|
||||
|
||||
def test_decorate_endpoint(self):
|
||||
@wamp.register("com.calculator.square")
|
||||
def square(_):
|
||||
"""Do nothing."""
|
||||
|
||||
self.assertTrue(hasattr(square, '_wampuris'))
|
||||
self.assertTrue(type(square._wampuris) == list)
|
||||
self.assertEqual(len(square._wampuris), 1)
|
||||
self.assertIsInstance(square._wampuris[0], Pattern)
|
||||
self.assertTrue(square._wampuris[0].is_endpoint())
|
||||
self.assertFalse(square._wampuris[0].is_handler())
|
||||
self.assertFalse(square._wampuris[0].is_exception())
|
||||
self.assertEqual(square._wampuris[0].uri(), "com.calculator.square")
|
||||
self.assertEqual(square._wampuris[0]._type, Pattern.URI_TYPE_EXACT)
|
||||
|
||||
@wamp.register("com.myapp.product.<product:int>.update")
|
||||
def update_product(product=None, label=None):
|
||||
"""Do nothing."""
|
||||
|
||||
self.assertTrue(hasattr(update_product, '_wampuris'))
|
||||
self.assertTrue(type(update_product._wampuris) == list)
|
||||
self.assertEqual(len(update_product._wampuris), 1)
|
||||
self.assertIsInstance(update_product._wampuris[0], Pattern)
|
||||
self.assertTrue(update_product._wampuris[0].is_endpoint())
|
||||
self.assertFalse(update_product._wampuris[0].is_handler())
|
||||
self.assertFalse(update_product._wampuris[0].is_exception())
|
||||
self.assertEqual(update_product._wampuris[0].uri(), "com.myapp.product.<product:int>.update")
|
||||
self.assertEqual(update_product._wampuris[0]._type, Pattern.URI_TYPE_WILDCARD)
|
||||
|
||||
@wamp.register("com.myapp.<category:string>.<cid:int>.update")
|
||||
def update(category=None, cid=None):
|
||||
"""Do nothing."""
|
||||
|
||||
self.assertTrue(hasattr(update, '_wampuris'))
|
||||
self.assertTrue(type(update._wampuris) == list)
|
||||
self.assertEqual(len(update._wampuris), 1)
|
||||
self.assertIsInstance(update._wampuris[0], Pattern)
|
||||
self.assertTrue(update._wampuris[0].is_endpoint())
|
||||
self.assertFalse(update._wampuris[0].is_handler())
|
||||
self.assertFalse(update._wampuris[0].is_exception())
|
||||
self.assertEqual(update._wampuris[0].uri(), "com.myapp.<category:string>.<cid:int>.update")
|
||||
self.assertEqual(update._wampuris[0]._type, Pattern.URI_TYPE_WILDCARD)
|
||||
|
||||
@wamp.register("com.myapp.circle.<name:string>",
|
||||
RegisterOptions(match="wildcard", details_arg="details"))
|
||||
def circle(name=None, details=None):
|
||||
""" Do nothing. """
|
||||
|
||||
self.assertTrue(hasattr(circle, '_wampuris'))
|
||||
self.assertTrue(type(circle._wampuris) == list)
|
||||
self.assertEqual(len(circle._wampuris), 1)
|
||||
self.assertIsInstance(circle._wampuris[0], Pattern)
|
||||
self.assertIsInstance(circle._wampuris[0].options, RegisterOptions)
|
||||
self.assertEqual(circle._wampuris[0].options.match, "wildcard")
|
||||
self.assertEqual(circle._wampuris[0].options.details_arg, "details")
|
||||
self.assertTrue(circle._wampuris[0].is_endpoint())
|
||||
self.assertFalse(circle._wampuris[0].is_handler())
|
||||
self.assertFalse(circle._wampuris[0].is_exception())
|
||||
self.assertEqual(circle._wampuris[0].uri(), "com.myapp.circle.<name:string>")
|
||||
self.assertEqual(circle._wampuris[0]._type, Pattern.URI_TYPE_WILDCARD)
|
||||
|
||||
@wamp.register("com.myapp.something..update",
|
||||
RegisterOptions(match="wildcard", details_arg="details"))
|
||||
def something(dynamic=None, details=None):
|
||||
""" Do nothing. """
|
||||
|
||||
self.assertTrue(hasattr(something, '_wampuris'))
|
||||
self.assertTrue(type(something._wampuris) == list)
|
||||
self.assertEqual(len(something._wampuris), 1)
|
||||
self.assertIsInstance(something._wampuris[0], Pattern)
|
||||
self.assertIsInstance(something._wampuris[0].options, RegisterOptions)
|
||||
self.assertEqual(something._wampuris[0].options.match, "wildcard")
|
||||
self.assertEqual(something._wampuris[0].options.details_arg, "details")
|
||||
self.assertTrue(something._wampuris[0].is_endpoint())
|
||||
self.assertFalse(something._wampuris[0].is_handler())
|
||||
self.assertFalse(something._wampuris[0].is_exception())
|
||||
self.assertEqual(something._wampuris[0].uri(), "com.myapp.something..update")
|
||||
self.assertEqual(something._wampuris[0]._type, Pattern.URI_TYPE_WILDCARD)
|
||||
|
||||
def test_decorate_handler(self):
|
||||
@wamp.subscribe("com.myapp.on_shutdown")
|
||||
def on_shutdown():
|
||||
"""Do nothing."""
|
||||
|
||||
self.assertTrue(hasattr(on_shutdown, '_wampuris'))
|
||||
self.assertTrue(type(on_shutdown._wampuris) == list)
|
||||
self.assertEqual(len(on_shutdown._wampuris), 1)
|
||||
self.assertIsInstance(on_shutdown._wampuris[0], Pattern)
|
||||
self.assertFalse(on_shutdown._wampuris[0].is_endpoint())
|
||||
self.assertTrue(on_shutdown._wampuris[0].is_handler())
|
||||
self.assertFalse(on_shutdown._wampuris[0].is_exception())
|
||||
self.assertEqual(on_shutdown._wampuris[0].uri(), "com.myapp.on_shutdown")
|
||||
self.assertEqual(on_shutdown._wampuris[0]._type, Pattern.URI_TYPE_EXACT)
|
||||
|
||||
@wamp.subscribe("com.myapp.product.<product:int>.on_update")
|
||||
def on_product_update(product=None, label=None):
|
||||
"""Do nothing."""
|
||||
|
||||
self.assertTrue(hasattr(on_product_update, '_wampuris'))
|
||||
self.assertTrue(type(on_product_update._wampuris) == list)
|
||||
self.assertEqual(len(on_product_update._wampuris), 1)
|
||||
self.assertIsInstance(on_product_update._wampuris[0], Pattern)
|
||||
self.assertFalse(on_product_update._wampuris[0].is_endpoint())
|
||||
self.assertTrue(on_product_update._wampuris[0].is_handler())
|
||||
self.assertFalse(on_product_update._wampuris[0].is_exception())
|
||||
self.assertEqual(on_product_update._wampuris[0].uri(), "com.myapp.product.<product:int>.on_update")
|
||||
self.assertEqual(on_product_update._wampuris[0]._type, Pattern.URI_TYPE_WILDCARD)
|
||||
|
||||
@wamp.subscribe("com.myapp.<category:string>.<cid:int>.on_update")
|
||||
def on_update(category=None, cid=None, label=None):
|
||||
"""Do nothing."""
|
||||
|
||||
self.assertTrue(hasattr(on_update, '_wampuris'))
|
||||
self.assertTrue(type(on_update._wampuris) == list)
|
||||
self.assertEqual(len(on_update._wampuris), 1)
|
||||
self.assertIsInstance(on_update._wampuris[0], Pattern)
|
||||
self.assertFalse(on_update._wampuris[0].is_endpoint())
|
||||
self.assertTrue(on_update._wampuris[0].is_handler())
|
||||
self.assertFalse(on_update._wampuris[0].is_exception())
|
||||
self.assertEqual(on_update._wampuris[0].uri(), "com.myapp.<category:string>.<cid:int>.on_update")
|
||||
self.assertEqual(on_update._wampuris[0]._type, Pattern.URI_TYPE_WILDCARD)
|
||||
|
||||
@wamp.subscribe("com.myapp.on.<event:string>",
|
||||
SubscribeOptions(match="wildcard", details_arg="details"))
|
||||
def on_event(event=None, details=None):
|
||||
""" Do nothing. """
|
||||
|
||||
self.assertTrue(hasattr(on_event, '_wampuris'))
|
||||
self.assertTrue(type(on_event._wampuris) == list)
|
||||
self.assertEqual(len(on_event._wampuris), 1)
|
||||
self.assertIsInstance(on_event._wampuris[0], Pattern)
|
||||
self.assertIsInstance(on_event._wampuris[0].options, SubscribeOptions)
|
||||
self.assertEqual(on_event._wampuris[0].options.match, "wildcard")
|
||||
self.assertEqual(on_event._wampuris[0].options.details_arg, "details")
|
||||
self.assertFalse(on_event._wampuris[0].is_endpoint())
|
||||
self.assertTrue(on_event._wampuris[0].is_handler())
|
||||
self.assertFalse(on_event._wampuris[0].is_exception())
|
||||
self.assertEqual(on_event._wampuris[0].uri(), "com.myapp.on.<event:string>")
|
||||
self.assertEqual(on_event._wampuris[0]._type, Pattern.URI_TYPE_WILDCARD)
|
||||
|
||||
def test_decorate_exception(self):
|
||||
@wamp.error("com.myapp.error")
|
||||
class AppError(Exception):
|
||||
"""Do nothing."""
|
||||
|
||||
self.assertTrue(hasattr(AppError, '_wampuris'))
|
||||
self.assertTrue(type(AppError._wampuris) == list)
|
||||
self.assertEqual(len(AppError._wampuris), 1)
|
||||
self.assertIsInstance(AppError._wampuris[0], Pattern)
|
||||
self.assertFalse(AppError._wampuris[0].is_endpoint())
|
||||
self.assertFalse(AppError._wampuris[0].is_handler())
|
||||
self.assertTrue(AppError._wampuris[0].is_exception())
|
||||
self.assertEqual(AppError._wampuris[0].uri(), "com.myapp.error")
|
||||
self.assertEqual(AppError._wampuris[0]._type, Pattern.URI_TYPE_EXACT)
|
||||
|
||||
@wamp.error("com.myapp.product.<product:int>.product_inactive")
|
||||
class ProductInactiveError(Exception):
|
||||
"""Do nothing."""
|
||||
|
||||
self.assertTrue(hasattr(ProductInactiveError, '_wampuris'))
|
||||
self.assertTrue(type(ProductInactiveError._wampuris) == list)
|
||||
self.assertEqual(len(ProductInactiveError._wampuris), 1)
|
||||
self.assertIsInstance(ProductInactiveError._wampuris[0], Pattern)
|
||||
self.assertFalse(ProductInactiveError._wampuris[0].is_endpoint())
|
||||
self.assertFalse(ProductInactiveError._wampuris[0].is_handler())
|
||||
self.assertTrue(ProductInactiveError._wampuris[0].is_exception())
|
||||
self.assertEqual(ProductInactiveError._wampuris[0].uri(), "com.myapp.product.<product:int>.product_inactive")
|
||||
self.assertEqual(ProductInactiveError._wampuris[0]._type, Pattern.URI_TYPE_WILDCARD)
|
||||
|
||||
@wamp.error("com.myapp.<category:string>.<product:int>.inactive")
|
||||
class ObjectInactiveError(Exception):
|
||||
"""Do nothing."""
|
||||
|
||||
self.assertTrue(hasattr(ObjectInactiveError, '_wampuris'))
|
||||
self.assertTrue(type(ObjectInactiveError._wampuris) == list)
|
||||
self.assertEqual(len(ObjectInactiveError._wampuris), 1)
|
||||
self.assertIsInstance(ObjectInactiveError._wampuris[0], Pattern)
|
||||
self.assertFalse(ObjectInactiveError._wampuris[0].is_endpoint())
|
||||
self.assertFalse(ObjectInactiveError._wampuris[0].is_handler())
|
||||
self.assertTrue(ObjectInactiveError._wampuris[0].is_exception())
|
||||
self.assertEqual(ObjectInactiveError._wampuris[0].uri(), "com.myapp.<category:string>.<product:int>.inactive")
|
||||
self.assertEqual(ObjectInactiveError._wampuris[0]._type, Pattern.URI_TYPE_WILDCARD)
|
||||
|
||||
def test_match_decorated_endpoint(self):
|
||||
@wamp.register("com.calculator.square")
|
||||
def square(x):
|
||||
return x
|
||||
|
||||
args, kwargs = square._wampuris[0].match("com.calculator.square")
|
||||
self.assertEqual(square(666, **kwargs), 666)
|
||||
|
||||
@wamp.register("com.myapp.product.<product:int>.update")
|
||||
def update_product(product=None, label=None):
|
||||
return product, label
|
||||
|
||||
args, kwargs = update_product._wampuris[0].match("com.myapp.product.123456.update")
|
||||
kwargs['label'] = "foobar"
|
||||
self.assertEqual(update_product(**kwargs), (123456, "foobar"))
|
||||
|
||||
@wamp.register("com.myapp.<category:string>.<cid:int>.update")
|
||||
def update(category=None, cid=None, label=None):
|
||||
return category, cid, label
|
||||
|
||||
args, kwargs = update._wampuris[0].match("com.myapp.product.123456.update")
|
||||
kwargs['label'] = "foobar"
|
||||
self.assertEqual(update(**kwargs), ("product", 123456, "foobar"))
|
||||
|
||||
def test_match_decorated_handler(self):
|
||||
@wamp.subscribe("com.myapp.on_shutdown")
|
||||
def on_shutdown():
|
||||
pass
|
||||
|
||||
args, kwargs = on_shutdown._wampuris[0].match("com.myapp.on_shutdown")
|
||||
self.assertEqual(on_shutdown(**kwargs), None)
|
||||
|
||||
@wamp.subscribe("com.myapp.product.<product:int>.on_update")
|
||||
def on_product_update(product=None, label=None):
|
||||
return product, label
|
||||
|
||||
args, kwargs = on_product_update._wampuris[0].match("com.myapp.product.123456.on_update")
|
||||
kwargs['label'] = "foobar"
|
||||
self.assertEqual(on_product_update(**kwargs), (123456, "foobar"))
|
||||
|
||||
@wamp.subscribe("com.myapp.<category:string>.<cid:int>.on_update")
|
||||
def on_update(category=None, cid=None, label=None):
|
||||
return category, cid, label
|
||||
|
||||
args, kwargs = on_update._wampuris[0].match("com.myapp.product.123456.on_update")
|
||||
kwargs['label'] = "foobar"
|
||||
self.assertEqual(on_update(**kwargs), ("product", 123456, "foobar"))
|
||||
|
||||
def test_match_decorated_exception(self):
|
||||
@wamp.error("com.myapp.error")
|
||||
class AppError(Exception):
|
||||
|
||||
def __init__(self, msg):
|
||||
Exception.__init__(self, msg)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.__class__ == other.__class__ and self.args == other.args
|
||||
|
||||
args, kwargs = AppError._wampuris[0].match("com.myapp.error")
|
||||
# noinspection PyArgumentList
|
||||
self.assertEqual(AppError("fuck", **kwargs), AppError("fuck"))
|
||||
|
||||
@wamp.error("com.myapp.product.<product:int>.product_inactive")
|
||||
class ProductInactiveError(Exception):
|
||||
|
||||
def __init__(self, msg, product=None):
|
||||
Exception.__init__(self, msg)
|
||||
self.product = product
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.__class__ == other.__class__ and self.args == other.args and self.product == other.product
|
||||
|
||||
args, kwargs = ProductInactiveError._wampuris[0].match("com.myapp.product.123456.product_inactive")
|
||||
self.assertEqual(ProductInactiveError("fuck", **kwargs), ProductInactiveError("fuck", 123456))
|
||||
|
||||
@wamp.error("com.myapp.<category:string>.<product:int>.inactive")
|
||||
class ObjectInactiveError(Exception):
|
||||
|
||||
def __init__(self, msg, category=None, product=None):
|
||||
Exception.__init__(self, msg)
|
||||
self.category = category
|
||||
self.product = product
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.__class__ == other.__class__ and self.args == other.args and \
|
||||
self.category == other.category and self.product == other.product
|
||||
|
||||
args, kwargs = ObjectInactiveError._wampuris[0].match("com.myapp.product.123456.inactive")
|
||||
self.assertEqual(ObjectInactiveError("fuck", **kwargs), ObjectInactiveError("fuck", "product", 123456))
|
||||
|
||||
|
||||
class KwException(Exception):
|
||||
def __init__(self, *args, **kwargs):
|
||||
Exception.__init__(self, *args)
|
||||
self.kwargs = kwargs
|
||||
|
||||
|
||||
# what if the WAMP error message received
|
||||
# contains args/kwargs that cannot be
|
||||
# consumed by the constructor of the exception
|
||||
# class defined for the WAMP error URI?
|
||||
|
||||
# 1. we can bail out (but we are already signaling an error)
|
||||
# 2. we can require a generic constructor
|
||||
# 3. we can map only unconsumed args/kwargs to generic attributes
|
||||
# 4. we can silently drop unconsumed args/kwargs
|
||||
|
||||
|
||||
class MockSession(object):
|
||||
|
||||
def __init__(self):
|
||||
self._ecls_to_uri_pat = {}
|
||||
self._uri_to_ecls = {}
|
||||
|
||||
def define(self, exception, error=None):
|
||||
if error is None:
|
||||
assert (hasattr(exception, '_wampuris'))
|
||||
self._ecls_to_uri_pat[exception] = exception._wampuris
|
||||
self._uri_to_ecls[exception._wampuris[0].uri()] = exception
|
||||
else:
|
||||
assert (not hasattr(exception, '_wampuris'))
|
||||
self._ecls_to_uri_pat[exception] = [Pattern(error, Pattern.URI_TARGET_HANDLER)]
|
||||
self._uri_to_ecls[error] = exception
|
||||
|
||||
def map_error(self, error, args=None, kwargs=None):
|
||||
|
||||
# FIXME:
|
||||
# 1. map to ecls based on error URI wildcard/prefix
|
||||
# 2. extract additional args/kwargs from error URI
|
||||
|
||||
if error in self._uri_to_ecls:
|
||||
ecls = self._uri_to_ecls[error]
|
||||
try:
|
||||
# the following might fail, eg. TypeError when
|
||||
# signature of exception constructor is incompatible
|
||||
# with args/kwargs or when the exception constructor raises
|
||||
if kwargs:
|
||||
if args:
|
||||
exc = ecls(*args, **kwargs)
|
||||
else:
|
||||
exc = ecls(**kwargs)
|
||||
else:
|
||||
if args:
|
||||
exc = ecls(*args)
|
||||
else:
|
||||
exc = ecls()
|
||||
except Exception:
|
||||
# FIXME: log e
|
||||
exc = KwException(error, *args, **kwargs)
|
||||
else:
|
||||
# this never fails
|
||||
args = args or []
|
||||
kwargs = kwargs or {}
|
||||
exc = KwException(error, *args, **kwargs)
|
||||
return exc
|
||||
|
||||
|
||||
class TestDecoratorsAdvanced(unittest.TestCase):
|
||||
|
||||
def test_decorate_exception_non_exception(self):
|
||||
|
||||
def test():
|
||||
# noinspection PyUnusedLocal
|
||||
@wamp.error("com.test.error")
|
||||
class Foo(object):
|
||||
pass
|
||||
|
||||
self.assertRaises(Exception, test)
|
||||
|
||||
def test_decorate_endpoint_multiple(self):
|
||||
|
||||
# noinspection PyUnusedLocal
|
||||
@wamp.register("com.oldapp.oldproc")
|
||||
@wamp.register("com.calculator.square")
|
||||
def square(x):
|
||||
"""Do nothing."""
|
||||
|
||||
self.assertTrue(hasattr(square, '_wampuris'))
|
||||
self.assertTrue(type(square._wampuris) == list)
|
||||
self.assertEqual(len(square._wampuris), 2)
|
||||
|
||||
for i in range(2):
|
||||
self.assertIsInstance(square._wampuris[i], Pattern)
|
||||
self.assertTrue(square._wampuris[i].is_endpoint())
|
||||
self.assertFalse(square._wampuris[i].is_handler())
|
||||
self.assertFalse(square._wampuris[i].is_exception())
|
||||
self.assertEqual(square._wampuris[i]._type, Pattern.URI_TYPE_EXACT)
|
||||
|
||||
self.assertEqual(square._wampuris[0].uri(), "com.calculator.square")
|
||||
self.assertEqual(square._wampuris[1].uri(), "com.oldapp.oldproc")
|
||||
|
||||
def test_marshal_decorated_exception(self):
|
||||
|
||||
@wamp.error("com.myapp.error")
|
||||
class AppError(Exception):
|
||||
pass
|
||||
|
||||
try:
|
||||
raise AppError("fuck")
|
||||
except Exception as e:
|
||||
self.assertEqual(e._wampuris[0].uri(), "com.myapp.error")
|
||||
|
||||
@wamp.error("com.myapp.product.<product:int>.product_inactive")
|
||||
class ProductInactiveError(Exception):
|
||||
|
||||
def __init__(self, msg, product=None):
|
||||
Exception.__init__(self, msg)
|
||||
self.product = product
|
||||
|
||||
try:
|
||||
raise ProductInactiveError("fuck", 123456)
|
||||
except Exception as e:
|
||||
self.assertEqual(e._wampuris[0].uri(), "com.myapp.product.<product:int>.product_inactive")
|
||||
|
||||
session = MockSession()
|
||||
session.define(AppError)
|
||||
|
||||
def test_define_exception_undecorated(self):
|
||||
|
||||
session = MockSession()
|
||||
|
||||
class AppError(Exception):
|
||||
pass
|
||||
|
||||
# defining an undecorated exception requires
|
||||
# an URI to be provided
|
||||
self.assertRaises(Exception, session.define, AppError)
|
||||
|
||||
session.define(AppError, "com.myapp.error")
|
||||
|
||||
exc = session.map_error("com.myapp.error")
|
||||
self.assertIsInstance(exc, AppError)
|
||||
|
||||
def test_define_exception_decorated(self):
|
||||
|
||||
session = MockSession()
|
||||
|
||||
@wamp.error("com.myapp.error")
|
||||
class AppError(Exception):
|
||||
pass
|
||||
|
||||
# when defining a decorated exception
|
||||
# an URI must not be provided
|
||||
self.assertRaises(Exception, session.define, AppError, "com.myapp.error")
|
||||
|
||||
session.define(AppError)
|
||||
|
||||
exc = session.map_error("com.myapp.error")
|
||||
self.assertIsInstance(exc, AppError)
|
||||
|
||||
def test_map_exception_undefined(self):
|
||||
|
||||
session = MockSession()
|
||||
|
||||
exc = session.map_error("com.myapp.error")
|
||||
self.assertIsInstance(exc, Exception)
|
||||
|
||||
def test_map_exception_args(self):
|
||||
|
||||
session = MockSession()
|
||||
|
||||
@wamp.error("com.myapp.error")
|
||||
class AppError(Exception):
|
||||
pass
|
||||
|
||||
@wamp.error("com.myapp.error.product_inactive")
|
||||
class ProductInactiveError(Exception):
|
||||
def __init__(self, product=None):
|
||||
self.product = product
|
||||
|
||||
# define exceptions in mock session
|
||||
session.define(AppError)
|
||||
session.define(ProductInactiveError)
|
||||
|
||||
for test in [
|
||||
# ("com.myapp.foo.error", [], {}, KwException),
|
||||
("com.myapp.error", [], {}, AppError),
|
||||
("com.myapp.error", ["you are doing it wrong"], {}, AppError),
|
||||
("com.myapp.error", ["you are doing it wrong", 1, 2, 3], {}, AppError),
|
||||
|
||||
("com.myapp.error.product_inactive", [], {}, ProductInactiveError),
|
||||
("com.myapp.error.product_inactive", [], {"product": 123456}, ProductInactiveError),
|
||||
]:
|
||||
error, args, kwargs, ecls = test
|
||||
exc = session.map_error(error, args, kwargs)
|
||||
|
||||
self.assertIsInstance(exc, ecls)
|
||||
self.assertEqual(list(exc.args), args)
|
||||
@@ -0,0 +1,406 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int 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.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
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
|
||||
from autobahn.wamp.types import TransportDetails
|
||||
|
||||
class MockTransport:
|
||||
def __init__(self):
|
||||
self.messages = []
|
||||
self._transport_details = TransportDetails()
|
||||
|
||||
def transport_details(self):
|
||||
return self._transport_details
|
||||
|
||||
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 = '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,
|
||||
{
|
||||
'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
|
||||
|
||||
# the whole variable must not be defined to deactivate (!)
|
||||
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("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("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("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("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("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.
|
||||
@@ -0,0 +1,40 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int 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.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
import os
|
||||
|
||||
if os.environ.get('USE_TWISTED', False):
|
||||
from twisted.trial import unittest
|
||||
|
||||
from autobahn.wamp.websocket import WampWebSocketProtocol
|
||||
|
||||
class TestWebsocketProtocol(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.protocol = WampWebSocketProtocol()
|
||||
|
||||
def test_close_before_open(self):
|
||||
# just checking this doesn't throw an exception...
|
||||
self.protocol.onClose(True, 1, "just testing")
|
||||
Reference in New Issue
Block a user