import json import pytest from fastapi import FastAPI from fastapi.testclient import TestClient from app.mobile.wifi import wifi_cipher, encode, install_wifi_route from app.mobile.protocol import ProtocolError from test_mobile_session import connection, request import base64 def opened(now=None): now = now or [0] manager, ble, calls = connection(lambda: now[0]) manager.wifi.address = lambda: '192.0.2.10' request(manager, ble, 1, '1', 'session.open', {'client_name': '手机'}) return manager, ble, calls, now def offer(manager, ble): result = request(manager, ble, 2, '2', 'transport.offer')['result'] client = wifi_cipher(base64.b64decode(result['secret']), result['channel_id'], server=False) hello = result['channel_id'].encode() + client.encrypt(encode({'device_id': 'test'})) return result, client, hello def test_upgrade_rpc_fallback_and_ble_lifetime(): m, ble, calls, now = opened() generation = m.generation o, client, hello = offer(m, ble) channel, ack = m.wifi.attach(hello) assert json.loads(client.decrypt(ack)) == {'device_id': 'test', 'ready': True} assert m.status()['active_transport'] == 'wifi' and m.generation == generation for number, method in enumerate(('status.get', 'content.play', 'transport.ping'), 1): now[0] += 5 packet = client.encrypt(encode(dict(id=str(number), method=method, params={}))) assert json.loads(client.decrypt(m.wifi.accept(channel, packet)))['ok'] assert calls == ['status.get', 'content.play'] now[0] = 20 assert m.expired() # WiFi traffic never refreshes BLE activity. with pytest.raises(ProtocolError): m.wifi.accept(channel, b'bad') m.disconnect('a') assert m.status()['active_transport'] is None def test_offer_expiry_identity_tamper_and_single_use(): m, ble, _, now = opened() o, c, hello = offer(m, ble) with pytest.raises(ProtocolError): m.wifi.attach(hello[:-1] + bytes([hello[-1] ^ 1])) assert m.wifi.pending is not None channel, _ = m.wifi.attach(hello) with pytest.raises(ProtocolError): m.wifi.attach(hello) assert m.wifi.valid(channel) # Attacker cannot evict the valid owner. assert request(m, ble, 3, '3', 'transport.close')['ok'] assert m.status()['active_transport'] == 'ble' with pytest.raises(ProtocolError): m.wifi.attach(hello) m, ble, _, now = opened() _, _, hello = offer(m, ble) now[0] = 10 with pytest.raises(ProtocolError): m.wifi.attach(hello) def test_no_wifi_address_and_wrong_identity(): m, ble, _, _ = opened() m.wifi.address = lambda: None assert not request(m, ble, 2, '2', 'transport.offer')['ok'] m.wifi.address = lambda: '198.51.100.20' o = request(m, ble, 3, '3', 'transport.offer')['result'] c = wifi_cipher(base64.b64decode(o['secret']), o['channel_id'], server=False) with pytest.raises(ProtocolError): m.wifi.attach(o['channel_id'].encode() + c.encrypt(encode({'device_id': 'other'}))) def test_duplicate_write_not_replayed_and_ble_only_commands_rejected(): m, ble, calls, _ = opened() o, c, hello = offer(m, ble) channel, ack = m.wifi.attach(hello); c.decrypt(ack) for rid, method, expected in [('1', 'content.play', True), ('1', 'content.play', False), ('2', 'wifi.set', False), ('3', 'session.ping', False)]: packet = c.encrypt(encode(dict(id=rid, method=method, params={}))) assert json.loads(c.decrypt(m.wifi.accept(channel, packet)))['ok'] is expected assert calls == ['content.play'] def test_real_websocket_records_and_disconnect(): m, ble, calls, _ = opened() app = FastAPI(); app.state.mobile_sessions = m; install_wifi_route(app) o, c, hello = offer(m, ble) with TestClient(app) as client: with client.websocket_connect('/ws/mobile') as ws: ws.send_bytes(hello) assert json.loads(c.decrypt(ws.receive_bytes()))['ready'] ws.send_bytes(c.encrypt(encode(dict(id='1', method='status.get', params={})))) assert json.loads(c.decrypt(ws.receive_bytes()))['ok'] m.disconnect('a') assert ws.receive()['type'] == 'websocket.close' assert calls == ['status.get'] def test_wifi_idle_expiry_and_cross_subnet_not_filtered(): m, ble, _, now = opened() m.wifi.address = lambda: '198.51.100.42' o, c, hello = offer(m, ble) assert o['host'] == '198.51.100.42' m.wifi.attach(hello) now[0] = 10 assert not m.wifi.valid() assert m.status()['active_transport'] == 'ble' def test_runtime_preserves_existing_transport_statistics(): from app.mobile.bluez import BluezRuntime m, ble, _, _ = opened() _, _, hello = offer(m, ble) m.wifi.attach(hello) status = BluezRuntime(m).status() assert status['active_transport'] == 'wifi' assert isinstance(status['transport'], dict) assert 'rx_bytes' in status['transport'] def test_public_wifi_vector_is_reproducible(): from pathlib import Path from generate_wifi_vector import generate assert generate() == json.loads((Path(__file__).parent / 'fixtures/protocol-wifi-v1.json').read_text(encoding='utf-8'))