Files
xianyan/scripts/file_transfer_full_test.dart
Developer 228095f80a chore: 完成v6.7.0版本迭代更新
本次更新涵盖多个功能模块的优化与新增:
1. 新增Wi-Fi直连配对方式与协作画布模块
2. 完成设备管理重命名API与前端适配
3. 优化日签卡片空数据保护与UI细节
4. 新增剪贴板工具与每日运势会话
5. 修复应用锁恢复、语音消息录制等已知问题
6. 完善文件传输统计与配对逻辑
7. 更新安卓权限配置与iOS隐私描述
8. 新增自动化测试脚本与文档
9. 清理旧版审计报告与测试文件
2026-05-14 05:35:18 +08:00

901 lines
27 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================
// 闲言APP — 文件传输全流程验证脚本 v3
// 创建时间: 2026-05-12
// 更新时间: 2026-05-13
// 作用: 模拟两个设备通过信令服务器互发消息/文件/数据
// 验证配对、消息发送、文件传输、送达回执、在线状态等全流程
// 上次更新: v3 新增deviceOnline/deviceOffline测试+text-message vs wsRelay对比
// 运行: dart run Scripts/file_transfer_full_test.dart
// ============================================================
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:web_socket_channel/web_socket_channel.dart';
const String kSignalingUrl = 'wss://tools.wktyl.com:9443';
const String kApiBase = 'https://tools.wktyl.com/api/file_transfer';
const Duration kTimeout = Duration(seconds: 10);
const Duration kHeartbeatInterval = Duration(seconds: 30);
class TestDevice {
TestDevice({required this.localId, required this.alias, this.userId});
final String localId;
String? serverId;
final String alias;
final String? userId;
WebSocketChannel? _channel;
bool _isConnected = false;
bool get isConnected => _isConnected;
String get effectiveId => serverId ?? localId;
final StreamController<Map<String, dynamic>> _messageController =
StreamController<Map<String, dynamic>>.broadcast();
Stream<Map<String, dynamic>> get onMessage => _messageController.stream;
final List<Map<String, dynamic>> allMessages = [];
Timer? _heartbeatTimer;
Future<bool> connect() async {
try {
print('[$alias] Connecting to $kSignalingUrl...');
_channel = WebSocketChannel.connect(Uri.parse(kSignalingUrl));
await _channel!.ready.timeout(kTimeout);
_channel!.stream.listen(
(data) {
_handleMessage(data as String);
},
onDone: () {
_isConnected = false;
print('[$alias] Connection closed');
},
onError: (error) {
_isConnected = false;
print('[$alias] Connection error: $error');
},
);
_isConnected = true;
_sendRegister();
_startHeartbeat();
print('[$alias] Connected, waiting for server ID...');
return true;
} catch (e) {
print('[$alias] Connection failed: $e');
return false;
}
}
void _sendRegister() {
_send({
'type': 'register',
'from': localId,
'payload': {
'alias': alias,
'fingerprint': 'test-fp-${localId.substring(0, 8)}',
'deviceType': 'headless',
'deviceModel': 'TestDevice',
if (userId != null) 'userId': userId,
},
'ts': DateTime.now().millisecondsSinceEpoch,
});
}
void _startHeartbeat() {
_heartbeatTimer?.cancel();
_heartbeatTimer = Timer.periodic(kHeartbeatInterval, (_) {
if (_isConnected) {
_send({
'type': 'heartbeat',
'from': effectiveId,
'ts': DateTime.now().millisecondsSinceEpoch,
});
}
});
}
void _handleMessage(String data) {
try {
final json = jsonDecode(data) as Map<String, dynamic>;
allMessages.add(json);
_messageController.add(json);
final type = json['type'] as String? ?? '';
final from = json['from'] as String? ?? json['sender'] as String? ?? '';
if (type == 'registered') {
final assignedId =
json['id'] as String? ??
json['payload']?['deviceId'] as String? ??
json['payload']?['id'] as String? ??
json['deviceId'] as String?;
if (assignedId != null && serverId == null) {
serverId = assignedId;
print('[$alias] Server assigned ID: $serverId');
}
}
if (type == 'display-name') {
final sid =
json['payload']?['id'] as String? ??
json['id'] as String? ??
json['payload']?['deviceId'] as String? ??
json['sender'] as String?;
if (sid != null && serverId == null) {
serverId = sid;
print('[$alias] Got ID from display-name: $serverId');
}
}
print('[$alias] Received: type=$type from=$from');
} catch (e) {
print('[$alias] Parse error: $e');
}
}
void _send(Map<String, dynamic> msg) {
if (!_isConnected || _channel == null) return;
_channel!.sink.add(jsonEncode(msg));
}
void sendTextMessage(String targetId, String text) {
_send({
'type': 'text-message',
'from': effectiveId,
'to': targetId,
'payload': {'text': text},
'ts': DateTime.now().millisecondsSinceEpoch,
});
print('[$alias] Sent text-message to $targetId: "$text"');
}
void sendWsRelayText(String targetId, String text) {
_send({
'type': 'wsRelay',
'from': effectiveId,
'to': targetId,
'relayType': 'text',
'payload': {'text': text},
'ts': DateTime.now().millisecondsSinceEpoch,
});
print('[$alias] Sent wsRelay text to $targetId: "$text"');
}
void sendFileMeta(
String targetId,
String fileName,
int fileSize,
String fileId,
) {
_send({
'type': 'wsRelay',
'from': effectiveId,
'to': targetId,
'relayType': 'file-meta',
'payload': {
'fileId': fileId,
'fileName': fileName,
'fileSize': fileSize,
'chunkSize': 65536,
'checksum': 'test-checksum',
},
'ts': DateTime.now().millisecondsSinceEpoch,
});
print('[$alias] Sent file meta to $targetId: $fileName ($fileSize bytes)');
}
void sendFileChunk(
String targetId,
String fileId,
int chunkIndex,
String base64Data,
) {
_send({
'type': 'wsRelay',
'from': effectiveId,
'to': targetId,
'relayType': 'file-chunk',
'payload': {
'fileId': fileId,
'chunkIndex': chunkIndex,
'data': base64Data,
},
'ts': DateTime.now().millisecondsSinceEpoch,
});
}
void sendFileComplete(String targetId, String fileId) {
_send({
'type': 'wsRelay',
'from': effectiveId,
'to': targetId,
'relayType': 'file-complete',
'payload': {'fileId': fileId},
'ts': DateTime.now().millisecondsSinceEpoch,
});
print('[$alias] Sent file complete to $targetId: $fileId');
}
void sendDeliveryAck(String targetId, String messageId) {
_send({
'type': 'delivery-ack',
'from': effectiveId,
'to': targetId,
'payload': {'messageId': messageId},
'ts': DateTime.now().millisecondsSinceEpoch,
});
print('[$alias] Sent delivery ack to $targetId for $messageId');
}
void sendPairRequest(String targetId) {
_send({
'type': 'pair-request',
'from': effectiveId,
'to': targetId,
'payload': {
'alias': alias,
'fingerprint': 'test-fp-${localId.substring(0, 8)}',
'deviceType': 'headless',
},
'ts': DateTime.now().millisecondsSinceEpoch,
});
print('[$alias] Sent pair request to $targetId');
}
void sendPairAccept(String targetId) {
_send({
'type': 'pair-accept',
'from': effectiveId,
'to': targetId,
'payload': {
'alias': alias,
'fingerprint': 'test-fp-${localId.substring(0, 8)}',
},
'ts': DateTime.now().millisecondsSinceEpoch,
});
print('[$alias] Sent pair accept to $targetId');
}
void sendPairReject(String targetId) {
_send({
'type': 'pair-reject',
'from': effectiveId,
'to': targetId,
'ts': DateTime.now().millisecondsSinceEpoch,
});
print('[$alias] Sent pair reject to $targetId');
}
void discover() {
_send({
'type': 'discover',
'from': effectiveId,
'ts': DateTime.now().millisecondsSinceEpoch,
});
print('[$alias] Sent discover request');
}
void ping(String targetId) {
_send({
'type': 'ping',
'from': effectiveId,
'to': targetId,
'ts': DateTime.now().millisecondsSinceEpoch,
});
print('[$alias] Sent ping to $targetId');
}
Future<void> disconnect() async {
_heartbeatTimer?.cancel();
if (_isConnected && _channel != null) {
_send({
'type': 'leave',
'from': effectiveId,
'ts': DateTime.now().millisecondsSinceEpoch,
});
await _channel?.sink.close();
}
_isConnected = false;
print('[$alias] Disconnected');
}
Future<Map<String, dynamic>?> waitForMessage(
String type, {
Duration timeout = kTimeout,
bool Function(Map<String, dynamic>)? filter,
}) async {
try {
return await onMessage
.where((msg) {
final msgType = msg['type'] as String? ?? '';
if (msgType != type) return false;
if (filter != null) return filter(msg);
return true;
})
.first
.timeout(timeout);
} on TimeoutException {
print('[$alias] Timeout waiting for message type: $type');
return null;
}
}
Future<bool> waitForRegistration({Duration timeout = kTimeout}) async {
if (serverId != null) return true;
for (final msg in allMessages) {
final type = msg['type'] as String? ?? '';
if (type == 'registered') {
final id =
msg['id'] as String? ??
msg['payload']?['deviceId'] as String? ??
msg['payload']?['id'] as String?;
if (id != null) {
serverId = id;
print('[$alias] Server assigned ID (from cache): $serverId');
return true;
}
}
}
try {
await onMessage
.where((msg) {
final type = msg['type'] as String? ?? '';
return type == 'registered';
})
.first
.timeout(timeout);
return serverId != null;
} on TimeoutException {
print('[$alias] Timeout waiting for registration');
return serverId != null;
}
}
}
class TestResult {
TestResult(this.name, this.passed, [this.detail]);
final String name;
final bool passed;
final String? detail;
@override
String toString() {
final icon = passed ? '' : '';
return '$icon $name${detail != null ? ': $detail' : ''}';
}
}
Future<void> main() async {
print('============================================================');
print(' 闲言APP 文件传输全流程验证脚本 v3');
print(' 时间: ${DateTime.now()}');
print(' 新增: deviceOnline/deviceOffline + text-message vs wsRelay对比');
print('============================================================\n');
final results = <TestResult>[];
final deviceA = TestDevice(
localId: 'test-a-${Random().nextInt(9999)}',
alias: 'TestDeviceA',
userId: 'test-user-a',
);
final deviceB = TestDevice(
localId: 'test-b-${Random().nextInt(9999)}',
alias: 'TestDeviceB',
userId: 'test-user-b',
);
// ---- Test 1: REST API Health Check ----
print('\n--- Test 1: REST API Health Check ---');
try {
final client = HttpClient();
final request = await client.getUrl(Uri.parse('$kApiBase/health'));
final response = await request.close().timeout(kTimeout);
final body = await response.transform(utf8.decoder).join();
client.close();
final healthy = response.statusCode == 200;
results.add(
TestResult('REST API Health', healthy, 'status=${response.statusCode} body=$body'),
);
print(' Health check: ${healthy ? "OK" : "FAIL"}');
} catch (e) {
results.add(TestResult('REST API Health', false, e.toString()));
}
// ---- Test 2: Signaling Info ----
print('\n--- Test 2: Signaling Info ---');
try {
final client = HttpClient();
final request = await client.getUrl(Uri.parse('$kApiBase/signaling_info'));
final response = await request.close().timeout(kTimeout);
final body = await response.transform(utf8.decoder).join();
client.close();
final json = jsonDecode(body) as Map<String, dynamic>;
results.add(
TestResult(
'Signaling Info',
json['code'] == 1,
'url=${json['data']?['signalingUrl']}',
),
);
} catch (e) {
results.add(TestResult('Signaling Info', false, e.toString()));
}
// ---- Test 3: Device A Connect & Register ----
print('\n--- Test 3: Device A Connect & Register ---');
final connectedA = await deviceA.connect();
results.add(TestResult('Device A Connect', connectedA));
if (connectedA) {
await Future.delayed(const Duration(seconds: 3));
final regA = await deviceA.waitForRegistration();
results.add(
TestResult(
'Device A Registered',
regA && deviceA.serverId != null,
'serverId=${deviceA.serverId}',
),
);
}
// ---- Test 4: Device B Connect & Register ----
print('\n--- Test 4: Device B Connect & Register ---');
final connectedB = await deviceB.connect();
results.add(TestResult('Device B Connect', connectedB));
if (connectedB) {
await Future.delayed(const Duration(seconds: 1));
final regB = await deviceB.waitForRegistration();
results.add(
TestResult(
'Device B Registered',
regB && deviceB.serverId != null,
'serverId=${deviceB.serverId}',
),
);
}
if (!connectedA ||
!connectedB ||
deviceA.serverId == null ||
deviceB.serverId == null) {
print('\n FATAL: Cannot proceed without both devices registered.');
await deviceA.disconnect();
await deviceB.disconnect();
_printResults(results);
return;
}
print('\n Device A server ID: ${deviceA.serverId}');
print(' Device B server ID: ${deviceB.serverId}');
// ---- Test 5: Discover Devices ----
print('\n--- Test 5: Discover Devices ---');
deviceA.discover();
final discoverResponse = await deviceA.waitForMessage(
'peers',
timeout: const Duration(seconds: 5),
);
results.add(
TestResult(
'Discover Response (peers)',
discoverResponse != null,
discoverResponse != null
? 'peers count=${(discoverResponse['payload']?['peers'] as List?)?.length ?? 0}'
: null,
),
);
// ---- Test 6: deviceOnline Event ----
print('\n--- Test 6: deviceOnline Event ---');
final onlineEvents = deviceA.allMessages.where(
(m) => m['type'] == 'deviceOnline',
).toList();
results.add(
TestResult(
'deviceOnline Event Received by A',
onlineEvents.isNotEmpty,
'count=${onlineEvents.length}',
),
);
if (onlineEvents.isNotEmpty) {
final event = onlineEvents.first;
results.add(
TestResult(
'deviceOnline Contains Device Info',
event['from'] != null || event['payload'] != null,
'from=${event['from']} payload=${event['payload']?.toString().substring(0, 50)}',
),
);
}
// ---- Test 7: Ping/Pong ----
print('\n--- Test 7: Ping/Pong ---');
deviceA.ping(deviceB.serverId!);
final pongReceived = await deviceA.waitForMessage(
'pong',
filter: (msg) => msg['from'] == deviceB.serverId,
timeout: const Duration(seconds: 5),
);
results.add(TestResult('Ping/Pong', pongReceived != null));
// ---- Test 8: Pairing Flow ----
print('\n--- Test 8: Pairing Flow ---');
deviceA.sendPairRequest(deviceB.serverId!);
final pairReqAtB = await deviceB.waitForMessage(
'pair-request',
filter: (msg) => msg['from'] == deviceA.serverId,
timeout: const Duration(seconds: 5),
);
results.add(
TestResult(
'Pair Request Received by B',
pairReqAtB != null,
pairReqAtB != null ? 'from=${pairReqAtB['from']}' : null,
),
);
if (pairReqAtB != null) {
deviceB.sendPairAccept(deviceA.serverId!);
final pairResponse = await deviceA.waitForMessage(
'pair-response',
timeout: const Duration(seconds: 5),
);
results.add(
TestResult(
'Pair Accept/Response Received by A',
pairResponse != null,
pairResponse != null
? 'from=${pairResponse['from']} status=${pairResponse['payload']?['status']}'
: null,
),
);
} else {
final pairResponseDirect = await deviceA.waitForMessage(
'pair-response',
timeout: const Duration(seconds: 3),
);
results.add(
TestResult(
'Pair Response (server-mediated)',
pairResponseDirect != null,
pairResponseDirect != null
? 'payload=${pairResponseDirect['payload']}'
: null,
),
);
}
// ---- Test 9: text-message A → B (旧协议) ----
print('\n--- Test 9: text-message A → B (旧协议) ---');
final testText1 = 'OLD-PROTOCOL from A! ${DateTime.now().millisecondsSinceEpoch}';
deviceA.sendTextMessage(deviceB.serverId!, testText1);
final textReceived1 = await deviceB.waitForMessage(
'text-message',
filter: (msg) => msg['from'] == deviceA.serverId,
timeout: const Duration(seconds: 5),
);
results.add(
TestResult(
'text-message A→B (旧协议)',
textReceived1 != null,
textReceived1 != null
? 'text="${(textReceived1['payload']?['text'] as String?)?.substring(0, 30)}..."'
: '❌ 旧协议text-message未送达! 这就是bug根因',
),
);
// ---- Test 10: wsRelay Text A → B (新协议) ----
print('\n--- Test 10: wsRelay Text A → B (新协议) ---');
final relayText1 = 'NEW-PROTOCOL from A! ${DateTime.now().millisecondsSinceEpoch}';
deviceA.sendWsRelayText(deviceB.serverId!, relayText1);
final relayReceived1 = await deviceB.waitForMessage(
'wsRelay',
filter: (msg) =>
msg['from'] == deviceA.serverId &&
(msg['relayType'] == 'text' || msg['payload']?['relayType'] == 'text'),
timeout: const Duration(seconds: 5),
);
results.add(
TestResult(
'wsRelay Text A→B (新协议)',
relayReceived1 != null,
relayReceived1 != null
? 'text="${(relayReceived1['payload']?['text'] as String?)?.substring(0, 30)}..."'
: null,
),
);
// ---- Test 11: wsRelay Text B → A ----
print('\n--- Test 11: wsRelay Text B → A ---');
final relayText2 = 'NEW-PROTOCOL from B! ${DateTime.now().millisecondsSinceEpoch}';
deviceB.sendWsRelayText(deviceA.serverId!, relayText2);
final relayReceived2 = await deviceA.waitForMessage(
'wsRelay',
filter: (msg) =>
msg['from'] == deviceB.serverId &&
(msg['relayType'] == 'text' || msg['payload']?['relayType'] == 'text'),
timeout: const Duration(seconds: 5),
);
results.add(
TestResult(
'wsRelay Text B→A',
relayReceived2 != null,
relayReceived2 != null
? 'text="${(relayReceived2['payload']?['text'] as String?)?.substring(0, 30)}..."'
: null,
),
);
// ---- Test 12: Protocol Comparison ----
print('\n--- Test 12: text-message vs wsRelay 协议对比 ---');
final oldProtocolWorks = textReceived1 != null;
final newProtocolWorks = relayReceived1 != null;
results.add(
TestResult(
'协议对比: wsRelay优于text-message',
newProtocolWorks,
'text-message=${oldProtocolWorks ? "" : ""} wsRelay=${newProtocolWorks ? "" : ""}',
),
);
if (!oldProtocolWorks && newProtocolWorks) {
print(' 💡 确认: text-message协议不可靠wsRelay协议可靠修复正确!');
}
// ---- Test 13: File Transfer via WsRelay A → B ----
print('\n--- Test 13: File Transfer via WsRelay A → B ---');
final fileId = 'test-file-${Random().nextInt(9999)}';
final fileName = 'test_file.txt';
final fileContent = 'This is a test file from device A. ' * 10;
final fileBytes = utf8.encode(fileContent);
final fileSize = fileBytes.length;
final chunkSize = 65536;
deviceA.sendFileMeta(deviceB.serverId!, fileName, fileSize, fileId);
final fileMetaReceived = await deviceB.waitForMessage(
'wsRelay',
filter: (msg) =>
msg['from'] == deviceA.serverId &&
(msg['relayType'] == 'file-meta' ||
msg['payload']?['relayType'] == 'file-meta'),
timeout: const Duration(seconds: 5),
);
results.add(
TestResult(
'File Meta Received by B',
fileMetaReceived != null,
fileMetaReceived != null
? 'fileName=${fileMetaReceived['payload']?['fileName']} fileSize=${fileMetaReceived['payload']?['fileSize']}'
: null,
),
);
if (fileMetaReceived != null) {
int offset = 0;
int chunkIndex = 0;
while (offset < fileBytes.length) {
final end = (offset + chunkSize < fileBytes.length)
? offset + chunkSize
: fileBytes.length;
final chunk = fileBytes.sublist(offset, end);
final base64Chunk = base64Encode(chunk);
deviceA.sendFileChunk(deviceB.serverId!, fileId, chunkIndex, base64Chunk);
offset = end;
chunkIndex++;
}
deviceA.sendFileComplete(deviceB.serverId!, fileId);
final fileCompleteReceived = await deviceB.waitForMessage(
'wsRelay',
filter: (msg) =>
msg['from'] == deviceA.serverId &&
(msg['relayType'] == 'file-complete' ||
msg['payload']?['relayType'] == 'file-complete'),
timeout: const Duration(seconds: 5),
);
results.add(
TestResult(
'File Complete Received by B',
fileCompleteReceived != null,
'chunks=$chunkIndex totalSize=$fileSize',
),
);
}
// ---- Test 14: Delivery Ack ----
print('\n--- Test 14: Delivery Ack ---');
if (relayReceived1 != null) {
deviceB.sendDeliveryAck(
deviceA.serverId!,
relayReceived1['ts']?.toString() ?? '',
);
final ackReceived = await deviceA.waitForMessage(
'delivery-ack',
filter: (msg) => msg['from'] == deviceB.serverId,
timeout: const Duration(seconds: 5),
);
results.add(TestResult('Delivery Ack Received by A', ackReceived != null));
} else {
results.add(TestResult('Delivery Ack', false, 'Skipped: no wsRelay message'));
}
// ---- Test 15: deviceOffline Event ----
print('\n--- Test 15: deviceOffline Event ---');
final deviceC = TestDevice(
localId: 'test-c-${Random().nextInt(9999)}',
alias: 'TestDeviceC',
);
final connectedC = await deviceC.connect();
if (connectedC) {
final regC = await deviceC.waitForRegistration();
await Future.delayed(const Duration(seconds: 1));
if (regC && deviceC.serverId != null) {
print(' Device C registered: ${deviceC.serverId}');
await deviceC.disconnect();
print(' Device C disconnected, waiting for deviceOffline...');
final offlineEvent = await deviceA.waitForMessage(
'deviceOffline',
filter: (msg) => msg['from'] == deviceC.serverId,
timeout: const Duration(seconds: 8),
);
results.add(
TestResult(
'deviceOffline Event',
offlineEvent != null,
offlineEvent != null
? 'from=${offlineEvent['from']}'
: '未收到deviceOffline广播',
),
);
} else {
results.add(TestResult('deviceOffline Event', false, 'Device C registration failed'));
}
} else {
results.add(TestResult('deviceOffline Event', false, 'Device C connect failed'));
}
// ---- Test 16: Pair Reject Flow ----
print('\n--- Test 16: Pair Reject Flow ---');
final deviceD = TestDevice(
localId: 'test-d-${Random().nextInt(9999)}',
alias: 'TestDeviceD',
);
final connectedD = await deviceD.connect();
if (connectedD) {
final regD = await deviceD.waitForRegistration();
if (regD && deviceD.serverId != null) {
await Future.delayed(const Duration(milliseconds: 500));
deviceD.sendPairRequest(deviceB.serverId!);
final pairReqD = await deviceB.waitForMessage(
'pair-request',
filter: (msg) => msg['from'] == deviceD.serverId,
timeout: const Duration(seconds: 5),
);
if (pairReqD != null) {
deviceB.sendPairReject(deviceD.serverId!);
final pairRejected = await deviceD.waitForMessage(
'pair-reject',
filter: (msg) => msg['from'] == deviceB.serverId,
timeout: const Duration(seconds: 5),
);
results.add(TestResult('Pair Reject Flow', pairRejected != null));
} else {
results.add(TestResult('Pair Reject Flow', false, 'Pair request not received'));
}
} else {
results.add(TestResult('Pair Reject Flow', false, 'Device D registration failed'));
}
await deviceD.disconnect();
} else {
results.add(TestResult('Pair Reject Flow', false, 'Device D connect failed'));
}
// ---- Test 17: REST API Pair Request ----
print('\n--- Test 17: REST API Pair Request ---');
try {
final client = HttpClient();
final request = await client.postUrl(Uri.parse('$kApiBase/pair_request'));
request.headers.set('Content-Type', 'application/json; charset=utf-8');
final body = jsonEncode({
'fromId': deviceA.serverId,
'toId': deviceB.serverId,
'alias': deviceA.alias,
'fingerprint': 'test-fp-a',
});
request.write(utf8.encode(body));
final response = await request.close().timeout(kTimeout);
final respBody = await response.transform(utf8.decoder).join();
client.close();
final json = jsonDecode(respBody) as Map<String, dynamic>;
results.add(
TestResult(
'REST Pair Request',
json['code'] == 1,
'code=${json['code']} msg=${json['msg']}',
),
);
} catch (e) {
results.add(TestResult('REST Pair Request', false, e.toString()));
}
// ---- Test 18: REST Paired Devices ----
print('\n--- Test 18: REST Paired Devices ---');
try {
final client = HttpClient();
final request = await client.getUrl(
Uri.parse('$kApiBase/paired_devices?deviceId=${deviceA.serverId}'),
);
final response = await request.close().timeout(kTimeout);
final respBody = await response.transform(utf8.decoder).join();
client.close();
final json = jsonDecode(respBody) as Map<String, dynamic>;
results.add(
TestResult(
'REST Paired Devices',
json['code'] == 1,
'code=${json['code']} devices=${json['data']?['devices']?.length ?? 0}',
),
);
} catch (e) {
results.add(TestResult('REST Paired Devices', false, e.toString()));
}
// ---- Cleanup ----
print('\n--- Cleanup ---');
await deviceA.disconnect();
await deviceB.disconnect();
// ---- Summary ----
_printResults(results);
}
void _printResults(List<TestResult> results) {
print('\n============================================================');
print(' 验证结果汇总');
print('============================================================');
int passed = 0;
int failed = 0;
for (final r in results) {
print(' $r');
if (r.passed) {
passed++;
} else {
failed++;
}
}
print('------------------------------------------------------------');
print(' 总计: ${results.length} 通过: $passed 失败: $failed');
if (results.isNotEmpty) {
print(' 通过率: ${(passed / results.length * 100).toStringAsFixed(1)}%');
}
print('============================================================');
if (failed > 0) {
print('\n❌ 失败项:');
for (final r in results.where((r) => !r.passed)) {
print(' - ${r.name}: ${r.detail ?? "无详情"}');
}
print('\n💡 建议检查:');
print(' 1. 信令服务器是否正确转发消息');
print(' 2. 设备ID是否使用服务器分配的ID');
print(' 3. wsRelay协议是否被服务器正确处理');
print(' 4. deviceOnline/deviceOffline广播是否正常');
}
}