|
@@ -51,11 +51,14 @@ class _ScriptedTcp:
|
|
|
def __init__(self, responses):
|
|
def __init__(self, responses):
|
|
|
self.responses = list(responses)
|
|
self.responses = list(responses)
|
|
|
self.sent = []
|
|
self.sent = []
|
|
|
|
|
+ self.sent_sdata = []
|
|
|
self.reconnections = []
|
|
self.reconnections = []
|
|
|
self.sdata = 0
|
|
self.sdata = 0
|
|
|
|
|
+ self.sdata_updates = []
|
|
|
|
|
|
|
|
async def send(self, *args):
|
|
async def send(self, *args):
|
|
|
self.sent.append(args)
|
|
self.sent.append(args)
|
|
|
|
|
+ self.sent_sdata.append(self.sdata)
|
|
|
|
|
|
|
|
async def recv(self, timeout):
|
|
async def recv(self, timeout):
|
|
|
if not self.responses:
|
|
if not self.responses:
|
|
@@ -65,6 +68,7 @@ class _ScriptedTcp:
|
|
|
|
|
|
|
|
def set_sdata(self, value):
|
|
def set_sdata(self, value):
|
|
|
self.sdata = value
|
|
self.sdata = value
|
|
|
|
|
+ self.sdata_updates.append(value)
|
|
|
|
|
|
|
|
async def reconnect(self, host, port):
|
|
async def reconnect(self, host, port):
|
|
|
self.reconnections.append((host, port))
|
|
self.reconnections.append((host, port))
|
|
@@ -89,6 +93,34 @@ def _server(**changes):
|
|
|
return ServerInfo(**values)
|
|
return ServerInfo(**values)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+async def _start_role_server_for_world_gate(world_port):
|
|
|
|
|
+ async def role_handler(reader, writer):
|
|
|
|
|
+ responses = [
|
|
|
|
|
+ _response(C.SC_529, recog=1, body=b"login-key/user-42"),
|
|
|
|
|
+ _response(C.SC_530, recog=149, sequence=1),
|
|
|
|
|
+ _response(
|
|
|
|
|
+ C.SC_520,
|
|
|
|
|
+ body=(
|
|
|
|
|
+ b'{"list":[{"chr":"Hero","UserID":"role-123",'
|
|
|
|
|
+ b'"last":1}]}'
|
|
|
|
|
+ ),
|
|
|
|
|
+ sequence=2,
|
|
|
|
|
+ ),
|
|
|
|
|
+ _response(C.SC_525, body=f"ignored/{world_port}".encode(), sequence=3),
|
|
|
|
|
+ ]
|
|
|
|
|
+ try:
|
|
|
|
|
+ for response in responses:
|
|
|
|
|
+ await _read_frame(reader)
|
|
|
|
|
+ writer.write(response)
|
|
|
|
|
+ await writer.drain()
|
|
|
|
|
+ finally:
|
|
|
|
|
+ writer.close()
|
|
|
|
|
+ await writer.wait_closed()
|
|
|
|
|
+
|
|
|
|
|
+ server = await asyncio.start_server(role_handler, "127.0.0.1", 0)
|
|
|
|
|
+ return server, server.sockets[0].getsockname()[1]
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.asyncio
|
|
|
async def test_new_login_existing_role_enters_world():
|
|
async def test_new_login_existing_role_enters_world():
|
|
|
role_frames = []
|
|
role_frames = []
|
|
@@ -343,27 +375,160 @@ async def test_old_login_reconnects_from_sc530_before_role_lookup():
|
|
|
assert tcp.sent[2][5] == b"account-1/151/7/110585/206923"
|
|
assert tcp.sent[2][5] == b"account-1/151/7/110585/206923"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+@pytest.mark.asyncio
|
|
|
|
|
+async def test_fresh_login_resets_reused_transport_sdata_before_cs2001():
|
|
|
|
|
+ tcp = _ScriptedTcp([(_header(C.SC_503, recog=-6), b"")])
|
|
|
|
|
+ tcp.sdata = 149
|
|
|
|
|
+ login = GameLogin(tcp, randint=lambda _low, _high: 1)
|
|
|
|
|
+
|
|
|
|
|
+ with pytest.raises(GameLoginError):
|
|
|
|
|
+ await login.login(
|
|
|
|
|
+ BoxSession(uid="u", token="t", sdkid="", signkey=""),
|
|
|
|
|
+ _server(),
|
|
|
|
|
+ timeout=0.05,
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ assert tcp.sent[0][0] == C.CS_2001
|
|
|
|
|
+ assert tcp.sent_sdata[0] == 0
|
|
|
|
|
+ assert tcp.sdata_updates[0] == 0
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@pytest.mark.asyncio
|
|
|
|
|
+async def test_stalled_send_is_cancelled_at_shared_login_deadline():
|
|
|
|
|
+ started = asyncio.Event()
|
|
|
|
|
+
|
|
|
|
|
+ class StalledSendTcp(_ScriptedTcp):
|
|
|
|
|
+ def __init__(self):
|
|
|
|
|
+ super().__init__([])
|
|
|
|
|
+ self.cancelled = False
|
|
|
|
|
+
|
|
|
|
|
+ async def send(self, *args):
|
|
|
|
|
+ started.set()
|
|
|
|
|
+ try:
|
|
|
|
|
+ await asyncio.Event().wait()
|
|
|
|
|
+ except asyncio.CancelledError:
|
|
|
|
|
+ self.cancelled = True
|
|
|
|
|
+ raise
|
|
|
|
|
+
|
|
|
|
|
+ tcp = StalledSendTcp()
|
|
|
|
|
+ login = GameLogin(tcp, randint=lambda _low, _high: 1)
|
|
|
|
|
+ loop = asyncio.get_running_loop()
|
|
|
|
|
+ began = loop.time()
|
|
|
|
|
+
|
|
|
|
|
+ with pytest.raises(TimeoutError):
|
|
|
|
|
+ await asyncio.wait_for(
|
|
|
|
|
+ login.login(
|
|
|
|
|
+ BoxSession(uid="u", token="t", sdkid="", signkey=""),
|
|
|
|
|
+ _server(),
|
|
|
|
|
+ timeout=0.02,
|
|
|
|
|
+ ),
|
|
|
|
|
+ timeout=0.2,
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ assert started.is_set()
|
|
|
|
|
+ assert loop.time() - began < 0.15
|
|
|
|
|
+ assert tcp.cancelled is True
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@pytest.mark.asyncio
|
|
|
|
|
+async def test_external_cancellation_is_not_converted_to_timeout():
|
|
|
|
|
+ started = asyncio.Event()
|
|
|
|
|
+
|
|
|
|
|
+ class StalledSendTcp(_ScriptedTcp):
|
|
|
|
|
+ async def send(self, *args):
|
|
|
|
|
+ started.set()
|
|
|
|
|
+ await asyncio.Event().wait()
|
|
|
|
|
+
|
|
|
|
|
+ login_task = asyncio.create_task(
|
|
|
|
|
+ GameLogin(StalledSendTcp([]), randint=lambda _low, _high: 1).login(
|
|
|
|
|
+ BoxSession(uid="u", token="t", sdkid="", signkey=""),
|
|
|
|
|
+ _server(),
|
|
|
|
|
+ timeout=1.0,
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ await asyncio.wait_for(started.wait(), timeout=0.1)
|
|
|
|
|
+ login_task.cancel()
|
|
|
|
|
+
|
|
|
|
|
+ with pytest.raises(asyncio.CancelledError):
|
|
|
|
|
+ await login_task
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@pytest.mark.asyncio
|
|
|
|
|
+async def test_stalled_reconnect_is_cancelled_at_shared_login_deadline():
|
|
|
|
|
+ class StalledReconnectTcp(_ScriptedTcp):
|
|
|
|
|
+ def __init__(self):
|
|
|
|
|
+ super().__init__(
|
|
|
|
|
+ [
|
|
|
|
|
+ (_header(C.SC_529, recog=0), b"unused/user-old"),
|
|
|
|
|
+ (_header(C.SC_530), b"ignored/7777/151"),
|
|
|
|
|
+ ]
|
|
|
|
|
+ )
|
|
|
|
|
+ self.cancelled = False
|
|
|
|
|
+
|
|
|
|
|
+ async def reconnect(self, host, port):
|
|
|
|
|
+ self.reconnections.append((host, port))
|
|
|
|
|
+ try:
|
|
|
|
|
+ await asyncio.Event().wait()
|
|
|
|
|
+ except asyncio.CancelledError:
|
|
|
|
|
+ self.cancelled = True
|
|
|
|
|
+ raise
|
|
|
|
|
+
|
|
|
|
|
+ tcp = StalledReconnectTcp()
|
|
|
|
|
+ login = GameLogin(tcp, randint=lambda _low, _high: 1)
|
|
|
|
|
+ loop = asyncio.get_running_loop()
|
|
|
|
|
+ began = loop.time()
|
|
|
|
|
+
|
|
|
|
|
+ with pytest.raises(TimeoutError):
|
|
|
|
|
+ await asyncio.wait_for(
|
|
|
|
|
+ login.login(
|
|
|
|
|
+ BoxSession(uid="u", token="t", sdkid="", signkey=""),
|
|
|
|
|
+ _server(),
|
|
|
|
|
+ timeout=0.02,
|
|
|
|
|
+ ),
|
|
|
|
|
+ timeout=0.2,
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ assert tcp.reconnections == [("game.example", 7777)]
|
|
|
|
|
+ assert loop.time() - began < 0.15
|
|
|
|
|
+ assert tcp.cancelled is True
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.asyncio
|
|
|
@pytest.mark.parametrize(
|
|
@pytest.mark.parametrize(
|
|
|
("responses", "char_name", "message_id", "error_code"),
|
|
("responses", "char_name", "message_id", "error_code"),
|
|
|
[
|
|
[
|
|
|
- ([(_header(C.SC_503, recog=-6), b"")], None, C.SC_503, -6),
|
|
|
|
|
|
|
+ (
|
|
|
|
|
+ [(_header(C.SC_503, recog=0, param1=101), b"")],
|
|
|
|
|
+ None,
|
|
|
|
|
+ C.SC_503,
|
|
|
|
|
+ 0,
|
|
|
|
|
+ ),
|
|
|
(
|
|
(
|
|
|
[
|
|
[
|
|
|
(_header(C.SC_529, recog=1), b"key/user"),
|
|
(_header(C.SC_529, recog=1), b"key/user"),
|
|
|
(_header(C.SC_530, recog=149), b""),
|
|
(_header(C.SC_530, recog=149), b""),
|
|
|
- (_header(C.SC_527, param1=3), b""),
|
|
|
|
|
|
|
+ (_header(C.SC_527, recog=202, param1=3), b""),
|
|
|
],
|
|
],
|
|
|
None,
|
|
None,
|
|
|
C.SC_527,
|
|
C.SC_527,
|
|
|
3,
|
|
3,
|
|
|
),
|
|
),
|
|
|
|
|
+ (
|
|
|
|
|
+ [
|
|
|
|
|
+ (_header(C.SC_529, recog=1), b"key/user"),
|
|
|
|
|
+ (_header(C.SC_530, recog=149), b""),
|
|
|
|
|
+ (_header(C.SC_527, recog=202, param1=0), b""),
|
|
|
|
|
+ ],
|
|
|
|
|
+ None,
|
|
|
|
|
+ C.SC_527,
|
|
|
|
|
+ 0,
|
|
|
|
|
+ ),
|
|
|
(
|
|
(
|
|
|
[
|
|
[
|
|
|
(_header(C.SC_529, recog=1), b"key/user"),
|
|
(_header(C.SC_529, recog=1), b"key/user"),
|
|
|
(_header(C.SC_530, recog=149), b""),
|
|
(_header(C.SC_530, recog=149), b""),
|
|
|
(_header(C.SC_520), b'{"list":[]}'),
|
|
(_header(C.SC_520), b'{"list":[]}'),
|
|
|
- (_header(C.SC_522, recog=-10), b""),
|
|
|
|
|
|
|
+ (_header(C.SC_522, recog=-10, param1=303), b""),
|
|
|
],
|
|
],
|
|
|
"New",
|
|
"New",
|
|
|
C.SC_522,
|
|
C.SC_522,
|
|
@@ -377,7 +542,7 @@ async def test_old_login_reconnects_from_sc530_before_role_lookup():
|
|
|
_header(C.SC_520),
|
|
_header(C.SC_520),
|
|
|
b'{"list":[{"chr":"Hero","UserID":"r1","last":1}]}',
|
|
b'{"list":[{"chr":"Hero","UserID":"r1","last":1}]}',
|
|
|
),
|
|
),
|
|
|
- (_header(C.SC_526, recog=99), b""),
|
|
|
|
|
|
|
+ (_header(C.SC_526, recog=99, param1=404), b""),
|
|
|
],
|
|
],
|
|
|
None,
|
|
None,
|
|
|
C.SC_526,
|
|
C.SC_526,
|
|
@@ -441,3 +606,125 @@ async def test_unrelated_messages_are_ignored_until_shared_deadline():
|
|
|
_server(),
|
|
_server(),
|
|
|
timeout=0.01,
|
|
timeout=0.01,
|
|
|
)
|
|
)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@pytest.mark.asyncio
|
|
|
|
|
+async def test_world_entry_waits_for_each_protocol_gate():
|
|
|
|
|
+ allow_sc658 = asyncio.Event()
|
|
|
|
|
+ allow_sc2 = asyncio.Event()
|
|
|
|
|
+ received_cs20 = asyncio.Event()
|
|
|
|
|
+ probing_cs1018 = asyncio.Event()
|
|
|
|
|
+ received_cs1018 = asyncio.Event()
|
|
|
|
|
+ world_done = asyncio.Event()
|
|
|
|
|
+ world_frames = []
|
|
|
|
|
+
|
|
|
|
|
+ async def world_handler(reader, writer):
|
|
|
|
|
+ try:
|
|
|
|
|
+ world_frames.append(await _read_frame(reader))
|
|
|
|
|
+ received_cs20.set()
|
|
|
|
|
+ cs1018_task = asyncio.create_task(_read_frame(reader))
|
|
|
|
|
+ probing_cs1018.set()
|
|
|
|
|
+ await allow_sc658.wait()
|
|
|
|
|
+ writer.write(_response(C.SC_658))
|
|
|
|
|
+ await writer.drain()
|
|
|
|
|
+ world_frames.append(await cs1018_task)
|
|
|
|
|
+ received_cs1018.set()
|
|
|
|
|
+ await allow_sc2.wait()
|
|
|
|
|
+ writer.write(_response(C.SC_2, sequence=1))
|
|
|
|
|
+ await writer.drain()
|
|
|
|
|
+ finally:
|
|
|
|
|
+ writer.close()
|
|
|
|
|
+ await writer.wait_closed()
|
|
|
|
|
+ world_done.set()
|
|
|
|
|
+
|
|
|
|
|
+ world_server = await asyncio.start_server(world_handler, "127.0.0.1", 0)
|
|
|
|
|
+ world_port = world_server.sockets[0].getsockname()[1]
|
|
|
|
|
+ role_server, role_port = await _start_role_server_for_world_gate(world_port)
|
|
|
|
|
+ tcp = TcpClient("127.0.0.1", role_port)
|
|
|
|
|
+ await tcp.connect()
|
|
|
|
|
+ login_task = asyncio.create_task(
|
|
|
|
|
+ GameLogin(tcp, randint=lambda _low, _high: 1).login(
|
|
|
|
|
+ BoxSession(uid="u", token="t", sdkid="", signkey=""),
|
|
|
|
|
+ _server(host="127.0.0.1", port=role_port),
|
|
|
|
|
+ timeout=1.0,
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ try:
|
|
|
|
|
+ await asyncio.wait_for(received_cs20.wait(), timeout=0.5)
|
|
|
|
|
+ await asyncio.wait_for(probing_cs1018.wait(), timeout=0.5)
|
|
|
|
|
+ with pytest.raises(TimeoutError):
|
|
|
|
|
+ await asyncio.wait_for(received_cs1018.wait(), timeout=0.02)
|
|
|
|
|
+ assert [header.msg_id for header, _ in world_frames] == [C.CS_20]
|
|
|
|
|
+ assert login_task.done() is False
|
|
|
|
|
+
|
|
|
|
|
+ allow_sc658.set()
|
|
|
|
|
+ await asyncio.wait_for(received_cs1018.wait(), timeout=0.5)
|
|
|
|
|
+ assert [header.msg_id for header, _ in world_frames] == [C.CS_20, C.CS_1018]
|
|
|
|
|
+ assert login_task.done() is False
|
|
|
|
|
+
|
|
|
|
|
+ allow_sc2.set()
|
|
|
|
|
+ result = await asyncio.wait_for(login_task, timeout=0.5)
|
|
|
|
|
+ assert result.world_ready is True
|
|
|
|
|
+ finally:
|
|
|
|
|
+ allow_sc658.set()
|
|
|
|
|
+ allow_sc2.set()
|
|
|
|
|
+ if not login_task.done():
|
|
|
|
|
+ login_task.cancel()
|
|
|
|
|
+ await tcp.close()
|
|
|
|
|
+ await asyncio.wait_for(world_done.wait(), timeout=0.5)
|
|
|
|
|
+ role_server.close()
|
|
|
|
|
+ world_server.close()
|
|
|
|
|
+ await role_server.wait_closed()
|
|
|
|
|
+ await world_server.wait_closed()
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@pytest.mark.asyncio
|
|
|
|
|
+@pytest.mark.parametrize(
|
|
|
|
|
+ ("withheld_message", "expected_frames"),
|
|
|
|
|
+ [(C.SC_658, [C.CS_20]), (C.SC_2, [C.CS_20, C.CS_1018])],
|
|
|
|
|
+)
|
|
|
|
|
+async def test_world_entry_times_out_while_gate_is_withheld(
|
|
|
|
|
+ withheld_message, expected_frames
|
|
|
|
|
+):
|
|
|
|
|
+ world_frames = []
|
|
|
|
|
+ world_done = asyncio.Event()
|
|
|
|
|
+
|
|
|
|
|
+ async def world_handler(reader, writer):
|
|
|
|
|
+ try:
|
|
|
|
|
+ world_frames.append(await _read_frame(reader))
|
|
|
|
|
+ if withheld_message == C.SC_2:
|
|
|
|
|
+ writer.write(_response(C.SC_658))
|
|
|
|
|
+ await writer.drain()
|
|
|
|
|
+ world_frames.append(await _read_frame(reader))
|
|
|
|
|
+ await asyncio.sleep(0.2)
|
|
|
|
|
+ else:
|
|
|
|
|
+ try:
|
|
|
|
|
+ world_frames.append(await _read_frame(reader))
|
|
|
|
|
+ except asyncio.IncompleteReadError:
|
|
|
|
|
+ pass
|
|
|
|
|
+ finally:
|
|
|
|
|
+ writer.close()
|
|
|
|
|
+ await writer.wait_closed()
|
|
|
|
|
+ world_done.set()
|
|
|
|
|
+
|
|
|
|
|
+ world_server = await asyncio.start_server(world_handler, "127.0.0.1", 0)
|
|
|
|
|
+ world_port = world_server.sockets[0].getsockname()[1]
|
|
|
|
|
+ role_server, role_port = await _start_role_server_for_world_gate(world_port)
|
|
|
|
|
+ tcp = TcpClient("127.0.0.1", role_port)
|
|
|
|
|
+ await tcp.connect()
|
|
|
|
|
+ try:
|
|
|
|
|
+ with pytest.raises(TimeoutError, match=f"SC {withheld_message}"):
|
|
|
|
|
+ await GameLogin(tcp, randint=lambda _low, _high: 1).login(
|
|
|
|
|
+ BoxSession(uid="u", token="t", sdkid="", signkey=""),
|
|
|
|
|
+ _server(host="127.0.0.1", port=role_port),
|
|
|
|
|
+ timeout=0.05,
|
|
|
|
|
+ )
|
|
|
|
|
+ finally:
|
|
|
|
|
+ await tcp.close()
|
|
|
|
|
+ await asyncio.wait_for(world_done.wait(), timeout=0.5)
|
|
|
|
|
+ role_server.close()
|
|
|
|
|
+ world_server.close()
|
|
|
|
|
+ await role_server.wait_closed()
|
|
|
|
|
+ await world_server.wait_closed()
|
|
|
|
|
+
|
|
|
|
|
+ assert [header.msg_id for header, _ in world_frames] == expected_frames
|