Fix: 1.16.5 is erroneously marked as unsupported
[pyCraft.git] / tests / test_connection.py
blobbb10b4efe316d9dcb687b87c5dc553330a6fa602
1 from minecraft import (
2 SUPPORTED_MINECRAFT_VERSIONS, SUPPORTED_PROTOCOL_VERSIONS,
3 PROTOCOL_VERSION_INDICES,
5 from minecraft.networking.packets import clientbound, serverbound
6 from minecraft.networking.connection import Connection
7 from minecraft.exceptions import (
8 VersionMismatch, LoginDisconnect, InvalidState, IgnorePacket
11 from . import fake_server
13 import sys
14 import re
15 import io
18 class ConnectTest(fake_server._FakeServerTest):
19 def test_connect(self):
20 self._test_connect()
22 class client_handler_type(fake_server.FakeClientHandler):
23 def handle_play_start(self):
24 super(ConnectTest.client_handler_type, self).handle_play_start()
25 self.write_packet(clientbound.play.KeepAlivePacket(
26 keep_alive_id=1223334444))
28 def handle_play_packet(self, packet):
29 super(ConnectTest.client_handler_type, self) \
30 .handle_play_packet(packet)
31 if isinstance(packet, serverbound.play.KeepAlivePacket):
32 assert packet.keep_alive_id == 1223334444
33 raise fake_server.FakeServerDisconnect
36 class ReconnectTest(ConnectTest):
37 phase = 0
39 def _start_client(self, client):
40 def handle_login_disconnect(packet):
41 if 'Please reconnect' in packet.json_data:
42 # Override the default behaviour of raising a fatal exception.
43 client.disconnect()
44 client.connect()
45 raise IgnorePacket
46 client.register_packet_listener(
47 handle_login_disconnect, clientbound.login.DisconnectPacket,
48 early=True)
50 def handle_play_disconnect(packet):
51 if 'Please reconnect' in packet.json_data:
52 client.connect()
53 elif 'Test successful' in packet.json_data:
54 raise fake_server.FakeServerTestSuccess
55 client.register_packet_listener(
56 handle_play_disconnect, clientbound.play.DisconnectPacket)
58 client.connect()
60 class client_handler_type(fake_server.FakeClientHandler):
61 def handle_login(self, packet):
62 if self.server.test_case.phase == 0:
63 self.server.test_case.phase = 1
64 raise fake_server.FakeServerDisconnect('Please reconnect (0).')
65 super(ReconnectTest.client_handler_type, self).handle_login(packet)
67 def handle_play_start(self):
68 if self.server.test_case.phase == 1:
69 self.server.test_case.phase = 2
70 raise fake_server.FakeServerDisconnect('Please reconnect (1).')
71 else:
72 assert self.server.test_case.phase == 2
73 raise fake_server.FakeServerDisconnect('Test successful (2).')
76 class PingTest(ConnectTest):
77 def _start_client(self, client):
78 def handle_ping(latency_ms):
79 assert 0 <= latency_ms < 60000
80 raise fake_server.FakeServerTestSuccess
81 client.status(handle_status=False, handle_ping=handle_ping)
84 class StatusTest(ConnectTest):
85 def _start_client(self, client):
86 def handle_status(status_dict):
87 assert status_dict['description'] == {'text': 'FakeServer'}
88 raise fake_server.FakeServerTestSuccess
89 client.status(handle_status=handle_status, handle_ping=False)
92 class DefaultStatusTest(ConnectTest):
93 def setUp(self):
94 class FakeStdOut(io.BytesIO):
95 def write(self, data):
96 if isinstance(data, str):
97 data = data.encode('utf8')
98 super(FakeStdOut, self).write(data)
99 sys.stdout, self.old_stdout = FakeStdOut(), sys.stdout
101 def tearDown(self):
102 sys.stdout, self.old_stdout = self.old_stdout, None
104 def _start_client(self, client):
105 def handle_exit():
106 output = sys.stdout.getvalue()
107 assert re.match(b'{.*}\\nPing: \\d+ ms\\n$', output), \
108 'Invalid stdout contents: %r.' % output
109 raise fake_server.FakeServerTestSuccess
110 client.handle_exit = handle_exit
112 client.status(handle_status=None, handle_ping=None)
115 class ConnectCompressionLowTest(ConnectTest):
116 compression_threshold = 0
119 class ConnectCompressionHighTest(ConnectTest):
120 compression_threshold = 256
123 class AllowedVersionsTest(fake_server._FakeServerTest):
124 versions = list(SUPPORTED_MINECRAFT_VERSIONS.items())
125 test_indices = (0, len(versions) // 2, len(versions) - 1)
127 client_handler_type = ConnectTest.client_handler_type
129 def test_with_version_names(self):
130 for index in self.test_indices:
131 self._test_connect(
132 server_version=self.versions[index][0],
133 client_versions={v[0] for v in self.versions[:index+1]})
135 def test_with_protocol_numbers(self):
136 for index in self.test_indices:
137 self._test_connect(
138 server_version=self.versions[index][0],
139 client_versions={v[1] for v in self.versions[:index+1]})
142 class LoginDisconnectTest(fake_server._FakeServerTest):
143 def test_login_disconnect(self):
144 with self.assertRaisesRegexp(LoginDisconnect, r'You are banned'):
145 self._test_connect()
147 class client_handler_type(fake_server.FakeClientHandler):
148 def handle_login(self, login_start_packet):
149 raise fake_server.FakeServerDisconnect('You are banned.')
152 class ConnectTwiceTest(fake_server._FakeServerTest):
153 def test_connect(self):
154 with self.assertRaisesRegexp(InvalidState, 'existing connection'):
155 self._test_connect()
157 class client_handler_type(fake_server.FakeClientHandler):
158 def handle_play_start(self):
159 super(ConnectTwiceTest.client_handler_type, self) \
160 .handle_play_start()
161 raise fake_server.FakeServerDisconnect('Test complete.')
163 def _start_client(self, client):
164 client.connect()
165 client.connect()
168 class ConnectStatusTest(ConnectTwiceTest):
169 def _start_client(self, client):
170 client.connect()
171 client.status()
174 class LoginPluginTest(fake_server._FakeServerTest):
175 class client_handler_type(fake_server.FakeClientHandler):
176 def handle_login(self, login_start_packet):
177 request = clientbound.login.PluginRequestPacket(
178 message_id=1, channel='pyCraft:tests/fail', data=b'ignored')
179 self.write_packet(request)
180 response = self.read_packet()
181 assert isinstance(response, serverbound.login.PluginResponsePacket)
182 assert response.message_id == request.message_id
183 assert response.successful is False
184 assert response.data is None
186 request = clientbound.login.PluginRequestPacket(
187 message_id=2, channel='pyCraft:tests/echo', data=b'hello')
188 self.write_packet(request)
189 response = self.read_packet()
190 assert isinstance(response, serverbound.login.PluginResponsePacket)
191 assert response.message_id == request.message_id
192 assert response.successful is True
193 assert response.data == request.data
195 super(LoginPluginTest.client_handler_type, self) \
196 .handle_login(login_start_packet)
198 def handle_play_start(self):
199 super(LoginPluginTest.client_handler_type, self) \
200 .handle_play_start()
201 raise fake_server.FakeServerDisconnect
203 def _start_client(self, client):
204 def handle_plugin_request(packet):
205 if packet.channel == 'pyCraft:tests/echo':
206 client.write_packet(serverbound.login.PluginResponsePacket(
207 message_id=packet.message_id, data=packet.data))
208 raise IgnorePacket
209 client.register_packet_listener(
210 handle_plugin_request, clientbound.login.PluginRequestPacket,
211 early=True)
213 super(LoginPluginTest, self)._start_client(client)
215 def test_login_plugin_messages(self):
216 self._test_connect()
219 class EarlyPacketListenerTest(ConnectTest):
220 """ Early packet listeners should be called before ordinary ones, even when
221 the early packet listener is registered afterwards.
223 def _start_client(self, client):
224 @client.listener(clientbound.play.JoinGamePacket)
225 def handle_join(packet):
226 assert early_handle_join.called, \
227 'Ordinary listener called before early listener.'
228 handle_join.called = True
229 handle_join.called = False
231 @client.listener(clientbound.play.JoinGamePacket, early=True)
232 def early_handle_join(packet):
233 early_handle_join.called = True
234 early_handle_join.called = False
236 @client.listener(clientbound.play.DisconnectPacket)
237 def handle_disconnect(packet):
238 assert early_handle_join.called, 'Early listener not called.'
239 assert handle_join.called, 'Ordinary listener not called.'
240 raise fake_server.FakeServerTestSuccess
242 client.connect()
245 class IgnorePacketTest(ConnectTest):
246 """ Raising 'minecraft.networking.connection.IgnorePacket' from within a
247 packet listener should prevent any subsequent packet listeners from
248 being called, and, if the listener is early, should prevent the default
249 behaviour from being triggered.
252 def _start_client(self, client):
253 keep_alive_ids_incoming = []
254 keep_alive_ids_outgoing = []
256 def handle_keep_alive_1(packet):
257 keep_alive_ids_incoming.append(packet.keep_alive_id)
258 if packet.keep_alive_id == 1:
259 raise IgnorePacket
260 client.register_packet_listener(
261 handle_keep_alive_1, clientbound.play.KeepAlivePacket, early=True)
263 def handle_keep_alive_2(packet):
264 keep_alive_ids_incoming.append(packet.keep_alive_id)
265 assert packet.keep_alive_id > 1
266 if packet.keep_alive_id == 2:
267 raise IgnorePacket
268 client.register_packet_listener(
269 handle_keep_alive_2, clientbound.play.KeepAlivePacket)
271 def handle_keep_alive_3(packet):
272 keep_alive_ids_incoming.append(packet.keep_alive_id)
273 assert packet.keep_alive_id == 3
274 client.register_packet_listener(
275 handle_keep_alive_3, clientbound.play.KeepAlivePacket)
277 def handle_outgoing_keep_alive_2(packet):
278 keep_alive_ids_outgoing.append(packet.keep_alive_id)
279 assert 2 <= packet.keep_alive_id <= 3
280 if packet.keep_alive_id == 2:
281 raise IgnorePacket
282 client.register_packet_listener(
283 handle_outgoing_keep_alive_2, serverbound.play.KeepAlivePacket,
284 outgoing=True, early=True)
286 def handle_outgoing_keep_alive_3(packet):
287 keep_alive_ids_outgoing.append(packet.keep_alive_id)
288 assert packet.keep_alive_id == 3
289 raise IgnorePacket
290 client.register_packet_listener(
291 handle_outgoing_keep_alive_3, serverbound.play.KeepAlivePacket,
292 outgoing=True)
294 def handle_outgoing_keep_alive_none(packet):
295 keep_alive_ids_outgoing.append(packet.keep_alive_id)
296 assert False
297 client.register_packet_listener(
298 handle_outgoing_keep_alive_none, serverbound.play.KeepAlivePacket,
299 outgoing=True)
301 def handle_disconnect(packet):
302 assert keep_alive_ids_incoming == [1, 2, 2, 3, 3, 3], \
303 'Incoming keep-alive IDs %r != %r' % \
304 (keep_alive_ids_incoming, [1, 2, 2, 3, 3, 3])
305 assert keep_alive_ids_outgoing == [2, 3, 3], \
306 'Outgoing keep-alive IDs %r != %r' % \
307 (keep_alive_ids_incoming, [2, 3, 3])
308 client.register_packet_listener(
309 handle_disconnect, clientbound.play.DisconnectPacket)
311 client.connect()
313 class client_handler_type(fake_server.FakeClientHandler):
314 __slots__ = '_keep_alive_ids_returned'
316 def __init__(self, *args, **kwds):
317 super(IgnorePacketTest.client_handler_type, self).__init__(
318 *args, **kwds)
319 self._keep_alive_ids_returned = []
321 def handle_play_start(self):
322 super(IgnorePacketTest.client_handler_type, self)\
323 .handle_play_start()
324 self.write_packet(clientbound.play.KeepAlivePacket(
325 keep_alive_id=1))
326 self.write_packet(clientbound.play.KeepAlivePacket(
327 keep_alive_id=2))
328 self.write_packet(clientbound.play.KeepAlivePacket(
329 keep_alive_id=3))
330 self.handle_play_server_disconnect('Test complete.')
332 def handle_play_packet(self, packet):
333 super(IgnorePacketTest.client_handler_type, self) \
334 .handle_play_packet(packet)
335 if isinstance(packet, serverbound.play.KeepAlivePacket):
336 self._keep_alive_ids_returned.append(packet.keep_alive_id)
338 def handle_play_client_disconnect(self):
339 assert self._keep_alive_ids_returned == [3], \
340 'Returned keep-alive IDs %r != %r' % \
341 (self._keep_alive_ids_returned, [3])
342 raise fake_server.FakeServerTestSuccess
345 class HandleExceptionTest(ConnectTest):
346 ignore_extra_exceptions = True
348 def _start_client(self, client):
349 message = 'Min skoldpadda ar inte snabb, men den ar en skoldpadda.'
351 @client.listener(clientbound.login.LoginSuccessPacket)
352 def handle_login_success(_packet):
353 raise Exception(message)
355 @client.exception_handler(early=True)
356 def handle_exception(exc, _exc_info):
357 assert isinstance(exc, Exception) and exc.args == (message,)
358 raise fake_server.FakeServerTestSuccess
360 client.connect()
363 class ExceptionReconnectTest(ConnectTest):
364 class CustomException(Exception):
365 pass
367 def setUp(self):
368 self.phase = 0
370 def _start_client(self, client):
371 @client.listener(clientbound.play.JoinGamePacket)
372 def handle_join_game(packet):
373 if self.phase == 0:
374 self.phase += 1
375 raise self.CustomException
376 else:
377 raise fake_server.FakeServerTestSuccess
379 @client.exception_handler(self.CustomException, early=True)
380 def handle_custom_exception(exc, exc_info):
381 client.disconnect(immediate=True)
382 client.connect()
384 client.connect()
386 class client_handler_type(ConnectTest.client_handler_type):
387 def handle_abnormal_disconnect(self, exc):
388 return True
391 class VersionNegotiationEdgeCases(fake_server._FakeServerTest):
392 earliest_version = SUPPORTED_PROTOCOL_VERSIONS[0]
393 latest_version = SUPPORTED_PROTOCOL_VERSIONS[-1]
395 fake_version = max(PROTOCOL_VERSION_INDICES.keys()) + 1
396 fake_version_index = max(PROTOCOL_VERSION_INDICES.values()) + 1
398 def setUp(self):
399 PROTOCOL_VERSION_INDICES[self.fake_version] = self.fake_version_index
400 super(VersionNegotiationEdgeCases, self).setUp()
402 def tearDown(self):
403 super(VersionNegotiationEdgeCases, self).tearDown()
404 del PROTOCOL_VERSION_INDICES[self.fake_version]
406 def test_client_protocol_unsupported(self):
407 self._test_client_protocol(version=self.fake_version)
409 def test_client_protocol_unknown(self):
410 self._test_client_protocol(version='surprise me!')
412 def test_client_protocol_invalid(self):
413 self._test_client_protocol(version=object())
415 def _test_client_protocol(self, version):
416 with self.assertRaisesRegexp(ValueError, 'Unsupported version'):
417 self._test_connect(client_versions={version})
419 def test_server_protocol_unsupported(self, client_versions=None):
420 with self.assertRaisesRegexp(VersionMismatch, 'not supported'):
421 self._test_connect(client_versions=client_versions,
422 server_version=self.fake_version)
424 def test_server_protocol_unsupported_direct(self):
425 self.test_server_protocol_unsupported({self.latest_version})
427 def test_server_protocol_disallowed(self, client_versions=None):
428 if client_versions is None:
429 client_versions = set(SUPPORTED_PROTOCOL_VERSIONS) \
430 - {self.latest_version}
431 with self.assertRaisesRegexp(VersionMismatch, 'not allowed'):
432 self._test_connect(client_versions={self.earliest_version},
433 server_version=self.latest_version)
435 def test_server_protocol_disallowed_direct(self):
436 self.test_server_protocol_disallowed({self.earliest_version})
438 def test_default_protocol_version(self, status_response=None):
439 if status_response is None:
440 status_response = '{"description": {"text": "FakeServer"}}'
442 class ClientHandler(fake_server.FakeClientHandler):
443 def handle_status(self, request_packet):
444 packet = clientbound.status.ResponsePacket()
445 packet.json_response = status_response
446 self.write_packet(packet)
448 def handle_play_start(self):
449 super(ClientHandler, self).handle_play_start()
450 raise fake_server.FakeServerDisconnect('Test complete.')
452 def make_connection(*args, **kwds):
453 kwds['initial_version'] = self.earliest_version
454 return Connection(*args, **kwds)
456 self._test_connect(server_version=self.earliest_version,
457 client_handler_type=ClientHandler,
458 connection_type=make_connection)
460 def test_default_protocol_version_empty(self):
461 with self.assertRaisesRegexp(IOError, 'Invalid server status'):
462 self.test_default_protocol_version(status_response='{}')
464 def test_default_protocol_version_eof(self):
465 class ClientHandler(fake_server.FakeClientHandler):
466 def handle_status(self, request_packet):
467 raise fake_server.FakeServerDisconnect(
468 'Refusing to handle status request, for test purposes.')
470 def handle_play_start(self):
471 super(ClientHandler, self).handle_play_start()
472 raise fake_server.FakeServerDisconnect('Test complete.')
474 def make_connection(*args, **kwds):
475 kwds['initial_version'] = self.earliest_version
476 return Connection(*args, **kwds)
478 self._test_connect(server_version=self.earliest_version,
479 client_handler_type=ClientHandler,
480 connection_type=make_connection)