Files
xianyan/scripts/signaling_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

671 lines
19 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 — 信令服务器接口测试脚本
// 创建时间: 2026-05-14
// 更新时间: 2026-05-14
// 作用: 测试WebSocket信令服务器核心接口
// - 连接/注册/设备发现/消息转发/文件元数据/配对/心跳
// - 验证discoverMyDevices去重逻辑
// 上次更新: 初始版本
// 运行: dart run Scripts/signaling_test.dart
// ============================================================
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:web_socket_channel/web_socket_channel.dart';
const String kSignalingUrl = 'wss://tools.wktyl.com:9443';
const Duration kTimeout = Duration(seconds: 10);
const Duration kHeartbeatInterval = Duration(seconds: 30);
int _passCount = 0;
int _failCount = 0;
void _result(String name, bool pass, {String? detail}) {
final icon = pass ? '' : '';
final status = pass ? 'PASS' : 'FAIL';
_passCount += pass ? 1 : 0;
_failCount += pass ? 0 : 1;
print('$icon [$status] $name${detail != null ? '$detail' : ''}');
}
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('\n🔗 [$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: (Object 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': 'SignalingTestDevice',
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? ?? '';
if (type == 'registered') {
final assignedId =
json['id'] as String? ??
json['payload']?['deviceId'] as String? ??
json['payload']?['id'] 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['sender'] as String?;
if (sid != null && serverId == null) {
serverId = sid;
print('[$alias] Got ID from display-name: $serverId');
}
}
} catch (_) {}
}
void _send(Map<String, dynamic> msg) {
if (!_isConnected || _channel == null) return;
_channel!.sink.add(jsonEncode(msg));
}
void sendDiscover() {
_send({
'type': 'discover',
'from': effectiveId,
'ts': DateTime.now().millisecondsSinceEpoch,
});
print('[$alias] Sent discover');
}
void sendDiscoverMyDevices(String uid) {
_send({
'type': 'discoverMyDevices',
'from': effectiveId,
'payload': {'userId': uid},
'ts': DateTime.now().millisecondsSinceEpoch,
});
print('[$alias] Sent discoverMyDevices for userId=$uid');
}
void sendTextMessage(String targetId, String text) {
_send({
'type': 'text-message',
'from': effectiveId,
'to': targetId,
'payload': {
'text': text,
'timestamp': DateTime.now().millisecondsSinceEpoch,
},
'ts': DateTime.now().millisecondsSinceEpoch,
});
print('[$alias] Sent text-message to $targetId');
}
void sendFileMeta(
String targetId, {
required String fileName,
required int fileSize,
required String mimeType,
required String taskId,
}) {
_send({
'type': 'file-meta',
'from': effectiveId,
'to': targetId,
'payload': {
'fileName': fileName,
'fileSize': fileSize,
'mimeType': mimeType,
'taskId': taskId,
},
'ts': DateTime.now().millisecondsSinceEpoch,
});
print('[$alias] Sent file-meta to $targetId: $fileName');
}
void sendPairRequest(String targetId) {
_send({
'type': 'pair-request',
'from': effectiveId,
'to': targetId,
'payload': {'pin': '123456'},
'ts': DateTime.now().millisecondsSinceEpoch,
});
print('[$alias] Sent pair-request to $targetId');
}
void sendPairResponse(String targetId, bool accepted) {
_send({
'type': 'pair-response',
'from': effectiveId,
'to': targetId,
'payload': {'accepted': accepted},
'ts': DateTime.now().millisecondsSinceEpoch,
});
print('[$alias] Sent pair-response to $targetId: accepted=$accepted');
}
void sendHeartbeat() {
_send({
'type': 'heartbeat',
'from': effectiveId,
'ts': DateTime.now().millisecondsSinceEpoch,
});
print('[$alias] Sent heartbeat');
}
Future<Map<String, dynamic>?> waitForMessage(
String type, {
Duration timeout = const Duration(seconds: 8),
}) async {
try {
return await onMessage
.firstWhere((m) => m['type'] == type)
.timeout(timeout);
} catch (_) {
return null;
}
}
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;
await _messageController.close();
}
}
Future<void> main() async {
print('╔══════════════════════════════════════════════════════════╗');
print('║ 闲言APP — 信令服务器接口测试 ║');
print('║ Signaling Server: $kSignalingUrl');
print('╚══════════════════════════════════════════════════════════╝\n');
await testWebSocketConnection();
await testDeviceRegister();
await testDiscoverMyDevicesDedup();
await testTextMessageForwarding();
await testFileMetaForwarding();
await testPairRequestResponse();
await testHeartbeat();
print('\n╔══════════════════════════════════════════════════════════╗');
print('║ 测试结果汇总 ║');
print('╠══════════════════════════════════════════════════════════╣');
print('║ ✅ 通过: $_passCount');
print('║ ❌ 失败: $_failCount');
print('║ 📊 总计: ${_passCount + _failCount}');
print('╚══════════════════════════════════════════════════════════╝');
exit(_failCount > 0 ? 1 : 0);
}
Future<void> testWebSocketConnection() async {
print('\n━━━ 1. WebSocket 连接测试 ━━━');
final device = TestDevice(
localId: 'conn-test-${DateTime.now().millisecondsSinceEpoch}',
alias: 'ConnTest',
);
final connected = await device.connect();
_result('WebSocket连接建立', connected);
if (connected) {
final registered = await device.waitForMessage('registered');
_result(
'收到registered响应',
registered != null,
detail: registered != null
? 'serverId=${registered['id'] ?? registered['payload']?['deviceId']}'
: '未收到registered消息',
);
}
await device.disconnect();
await Future<void>.delayed(const Duration(seconds: 1));
}
Future<void> testDeviceRegister() async {
print('\n━━━ 2. 设备注册测试 ━━━');
final device = TestDevice(
localId: 'reg-test-${DateTime.now().millisecondsSinceEpoch}',
alias: 'RegisterTest',
userId: 'test_user_register',
);
final connected = await device.connect();
_result('注册设备连接', connected);
if (connected) {
final registered = await device.waitForMessage('registered');
_result(
'注册成功收到确认',
registered != null,
detail: registered != null
? 'payload=${registered['payload']}'
: '超时未收到',
);
final hasServerId = device.serverId != null;
_result('服务器分配ID', hasServerId, detail: 'serverId=${device.serverId}');
}
await device.disconnect();
await Future<void>.delayed(const Duration(seconds: 1));
}
Future<void> testDiscoverMyDevicesDedup() async {
print('\n━━━ 3. 设备发现测试discoverMyDevices 去重验证) ━━━');
final testUserId = 'dedup_test_user_${DateTime.now().millisecondsSinceEpoch}';
final device1 = TestDevice(
localId: 'dedup-a-${DateTime.now().millisecondsSinceEpoch}',
alias: 'DedupDevice-A',
userId: testUserId,
);
final device2 = TestDevice(
localId: 'dedup-b-${DateTime.now().millisecondsSinceEpoch}',
alias: 'DedupDevice-B',
userId: testUserId,
);
final c1 = await device1.connect();
final c2 = await device2.connect();
_result('两台设备均连接成功', c1 && c2);
if (!c1 || !c2) {
await device1.disconnect();
await device2.disconnect();
return;
}
await Future<void>.delayed(const Duration(seconds: 2));
final discoverer = TestDevice(
localId: 'dedup-disc-${DateTime.now().millisecondsSinceEpoch}',
alias: 'Discoverer',
userId: testUserId,
);
final c3 = await discoverer.connect();
_result('发现者设备连接', c3);
if (!c3) {
await device1.disconnect();
await device2.disconnect();
return;
}
await Future<void>.delayed(const Duration(seconds: 1));
discoverer.sendDiscoverMyDevices(testUserId);
final response = await discoverer.waitForMessage('myDevicesResponse');
_result(
'收到myDevicesResponse',
response != null,
detail: response != null ? 'raw=${jsonEncode(response)}' : '超时未收到',
);
if (response != null) {
final devicesList =
(response['devices'] as List<dynamic>?) ??
(response['payload']?['devices'] as List<dynamic>?) ??
[];
_result(
'发现设备数量≥1',
devicesList.isNotEmpty,
detail: '发现${devicesList.length}台设备',
);
final fingerprints = <String>{};
var duplicateCount = 0;
for (final d in devicesList) {
if (d is Map<String, dynamic>) {
final fp =
d['fingerprint'] as String? ?? d['id'] as String? ?? '';
if (fp.isNotEmpty) {
if (fingerprints.contains(fp)) {
duplicateCount++;
print(' ⚠️ 发现重复设备: fingerprint=$fp');
}
fingerprints.add(fp);
}
}
}
_result(
'去重验证: 无重复设备',
duplicateCount == 0,
detail: duplicateCount > 0
? '发现$duplicateCount个重复项'
: '所有设备fingerprint唯一',
);
}
await device1.disconnect();
await device2.disconnect();
await discoverer.disconnect();
await Future<void>.delayed(const Duration(seconds: 1));
}
Future<void> testTextMessageForwarding() async {
print('\n━━━ 4. 文本消息转发测试 ━━━');
final sender = TestDevice(
localId: 'txt-snd-${DateTime.now().millisecondsSinceEpoch}',
alias: 'TextSender',
);
final receiver = TestDevice(
localId: 'txt-rcv-${DateTime.now().millisecondsSinceEpoch}',
alias: 'TextReceiver',
);
final c1 = await sender.connect();
final c2 = await receiver.connect();
_result('发送方和接收方连接', c1 && c2);
if (!c1 || !c2) {
await sender.disconnect();
await receiver.disconnect();
return;
}
await Future<void>.delayed(const Duration(seconds: 2));
final targetId = receiver.serverId ?? receiver.localId;
final testText = 'Hello from signaling_test at ${DateTime.now()}';
sender.sendTextMessage(targetId, testText);
final received = await receiver.waitForMessage('text-message');
_result(
'接收方收到text-message',
received != null,
detail: received != null
? 'from=${received['from']}, text=${received['payload']?['text']}'
: '超时未收到',
);
if (received != null) {
final receivedText = received['payload']?['text'] as String? ?? '';
_result(
'消息内容匹配',
receivedText == testText,
detail: receivedText == testText
? '内容一致'
: '不匹配: 期望"$testText", 实际"$receivedText"',
);
}
await sender.disconnect();
await receiver.disconnect();
await Future<void>.delayed(const Duration(seconds: 1));
}
Future<void> testFileMetaForwarding() async {
print('\n━━━ 5. 文件元数据转发测试 ━━━');
final sender = TestDevice(
localId: 'file-snd-${DateTime.now().millisecondsSinceEpoch}',
alias: 'FileSender',
);
final receiver = TestDevice(
localId: 'file-rcv-${DateTime.now().millisecondsSinceEpoch}',
alias: 'FileReceiver',
);
final c1 = await sender.connect();
final c2 = await receiver.connect();
_result('文件发送方和接收方连接', c1 && c2);
if (!c1 || !c2) {
await sender.disconnect();
await receiver.disconnect();
return;
}
await Future<void>.delayed(const Duration(seconds: 2));
final targetId = receiver.serverId ?? receiver.localId;
const testFileName = 'test_document.pdf';
const testFileSize = 1048576;
const testMimeType = 'application/pdf';
const testTaskId = 'task-file-meta-test-001';
sender.sendFileMeta(
targetId,
fileName: testFileName,
fileSize: testFileSize,
mimeType: testMimeType,
taskId: testTaskId,
);
final received = await receiver.waitForMessage('file-meta');
_result(
'接收方收到file-meta',
received != null,
detail: received != null
? 'from=${received['from']}, fileName=${received['payload']?['fileName']}'
: '超时未收到',
);
if (received != null) {
final payload = received['payload'] as Map<String, dynamic>? ?? {};
_result(
'文件名匹配',
payload['fileName'] == testFileName,
detail: '期望"$testFileName", 实际"${payload['fileName']}"',
);
_result(
'文件大小匹配',
payload['fileSize'] == testFileSize,
detail: '期望$testFileSize, 实际${payload['fileSize']}',
);
_result(
'MIME类型匹配',
payload['mimeType'] == testMimeType,
detail: '期望"$testMimeType", 实际"${payload['mimeType']}"',
);
_result(
'任务ID匹配',
payload['taskId'] == testTaskId,
detail: '期望"$testTaskId", 实际"${payload['taskId']}"',
);
}
await sender.disconnect();
await receiver.disconnect();
await Future<void>.delayed(const Duration(seconds: 1));
}
Future<void> testPairRequestResponse() async {
print('\n━━━ 6. 配对请求/接受测试 ━━━');
final deviceA = TestDevice(
localId: 'pair-a-${DateTime.now().millisecondsSinceEpoch}',
alias: 'PairDevice-A',
);
final deviceB = TestDevice(
localId: 'pair-b-${DateTime.now().millisecondsSinceEpoch}',
alias: 'PairDevice-B',
);
final c1 = await deviceA.connect();
final c2 = await deviceB.connect();
_result('配对双方连接', c1 && c2);
if (!c1 || !c2) {
await deviceA.disconnect();
await deviceB.disconnect();
return;
}
await Future<void>.delayed(const Duration(seconds: 2));
final targetB = deviceB.serverId ?? deviceB.localId;
deviceA.sendPairRequest(targetB);
final pairReq = await deviceB.waitForMessage('pair-request');
_result(
'B收到配对请求',
pairReq != null,
detail: pairReq != null
? 'from=${pairReq['from']}, pin=${pairReq['payload']?['pin']}'
: '超时未收到',
);
if (pairReq != null) {
final targetA = deviceA.serverId ?? deviceA.localId;
deviceB.sendPairResponse(targetA, true);
final pairResp = await deviceA.waitForMessage('pair-response');
_result(
'A收到配对响应',
pairResp != null,
detail: pairResp != null
? 'accepted=${pairResp['payload']?['accepted']}'
: '超时未收到',
);
if (pairResp != null) {
final accepted = pairResp['payload']?['accepted'] as bool? ?? false;
_result('配对已接受', accepted, detail: 'accepted=$accepted');
}
}
await deviceA.disconnect();
await deviceB.disconnect();
await Future<void>.delayed(const Duration(seconds: 1));
}
Future<void> testHeartbeat() async {
print('\n━━━ 7. 心跳测试 ━━━');
final device = TestDevice(
localId: 'hb-test-${DateTime.now().millisecondsSinceEpoch}',
alias: 'HeartbeatTest',
);
final connected = await device.connect();
_result('心跳测试设备连接', connected);
if (!connected) {
await device.disconnect();
return;
}
await Future<void>.delayed(const Duration(seconds: 1));
device.sendHeartbeat();
print(' 📤 已发送心跳包, 等待服务器响应...');
await Future<void>.delayed(const Duration(seconds: 3));
final stillConnected = device.isConnected;
_result(
'心跳后连接仍保持',
stillConnected,
detail: stillConnected ? '连接正常' : '连接已断开',
);
final pingReceived = await device.waitForMessage('ping',
timeout: const Duration(seconds: 5));
_result(
'收到服务器ping',
pingReceived != null,
detail: pingReceived != null
? '服务器主动ping, 连接保活正常'
: '未收到ping(可能服务器不主动ping, 连接仍正常)',
);
await device.disconnect();
await Future<void>.delayed(const Duration(seconds: 1));
}