import json import pytest from app.mobile.protocol import Handshake, ProtocolError, fragments from app.mobile.session import SessionManager def connection(clock=lambda: 0, *, manager=None, on_session_open=None): calls = [] if manager is None: manager = SessionManager(lambda method, params: calls.append(method) or {}, lambda: {"device_id": "test"}, clock=clock, on_session_open=on_session_open) assert manager.connect('a') assert not manager.connect('b') client = Handshake() result = None for part in fragments(1, 0, client.hello, 247): result = manager.accept('a', part) cipher = client.finish(result[1], server=False) return manager, cipher, calls def request(manager, cipher, mid, rid, method, params=None): raw = json.dumps(dict(id=rid, method=method, params=params or {})).encode() for part in fragments(3, mid, cipher.encrypt(raw), 247): result = manager.accept('a', part) return json.loads(cipher.decrypt(result[1])) def test_no_control_before_open(): manager, cipher, calls = connection() with pytest.raises(ProtocolError): request(manager, cipher, 1, '1', 'settings.patch') assert calls == [] def test_single_owner_duplicate_and_disconnect(): manager, cipher, calls = connection() assert request(manager, cipher, 1, '1', 'session.open', {'client_name': '手机'})['ok'] assert manager.status()['client_name'] == '手机' assert request(manager, cipher, 2, '2', 'content.play')['ok'] assert not request(manager, cipher, 3, '2', 'content.play')['ok'] assert calls == ['content.play'] manager.disconnect('b') assert manager.status()['connected'] manager.disconnect('a') assert not manager.status()['connected'] assert manager.connect('b') def test_handshake_and_idle_deadlines(): now = [0] manager, cipher, _ = connection(lambda: now[0]) now[0] = 10 assert manager.expired() manager, cipher, _ = connection(lambda: now[0]) request(manager, cipher, 1, '1', 'session.open', {'client_name': '手机'}) now[0] = 29 assert not manager.expired() now[0] = 30 assert manager.expired() def test_negotiated_receive_mtu_resets_on_disconnect(): manager, cipher, _ = connection() assert manager.peer_mtu == 23 assert request(manager, cipher, 1, '1', 'session.open', {'client_name': '手机', 'receive_mtu': 247})['ok'] assert manager.peer_mtu == 247 manager.disconnect('a') assert manager.peer_mtu == 23 @pytest.mark.parametrize('mtu', [0, 22, 518, True, '247']) def test_invalid_receive_mtu_never_opens_session(mtu): opened = [] manager, cipher, _ = connection(on_session_open=lambda: opened.append(True)) assert not request(manager, cipher, 1, '1', 'session.open', {'client_name': '手机', 'receive_mtu': mtu})['ok'] assert not manager.opened assert opened == [] def test_open_notification_only_after_valid_encrypted_session(): opened = [] manager, cipher, _ = connection(on_session_open=lambda: opened.append(manager.status()['connected'])) assert opened == [] # Neither the link nor the handshake is a session. assert not request(manager, cipher, 1, '1', 'session.open', {'client_name': ''})['ok'] assert opened == [] assert request(manager, cipher, 2, '2', 'session.open', {'client_name': '手机'})['ok'] assert opened == [True] assert not request(manager, cipher, 3, '3', 'session.open', {'client_name': '手机'})['ok'] request(manager, cipher, 4, '4', 'session.ping') manager.disconnect('a') assert opened == [True] def test_failed_handshake_never_notifies(): opened = [] manager = SessionManager(lambda *args: {}, lambda: {}, on_session_open=lambda: opened.append(True)) assert manager.connect('a') with pytest.raises(ProtocolError): for part in fragments(1, 0, b'invalid-handshake', 247): manager.accept('a', part) assert not manager.opened assert opened == [] def test_notification_failure_does_not_break_open_session(caplog): def failed_notification(): raise OSError('test notification failure') manager, cipher, _ = connection(on_session_open=failed_notification) assert request(manager, cipher, 1, '1', 'session.open', {'client_name': '手机'})['ok'] assert request(manager, cipher, 2, '2', 'session.ping')['ok'] assert 'session-open notification failed' in caplog.text