358 lines
13 KiB
Dart
358 lines
13 KiB
Dart
import 'dart:convert';
|
||
|
||
import 'package:crypto/crypto.dart';
|
||
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
|
||
import 'package:flutter_test/flutter_test.dart';
|
||
|
||
void main() {
|
||
group('OnlyOfficeViewer - 辅助函数单元测试', () {
|
||
test('文档类型识别 - Word 文档', () {
|
||
expect(TestHelper.getDocumentType('doc'), 'word');
|
||
expect(TestHelper.getDocumentType('docx'), 'word');
|
||
expect(TestHelper.getDocumentType('pdf'), 'word');
|
||
expect(TestHelper.getDocumentType('txt'), 'word');
|
||
expect(TestHelper.getDocumentType('rtf'), 'word');
|
||
expect(TestHelper.getDocumentType('html'), 'word');
|
||
expect(TestHelper.getDocumentType('epub'), 'word');
|
||
});
|
||
|
||
test('文档类型识别 - Excel 文档', () {
|
||
expect(TestHelper.getDocumentType('xls'), 'cell');
|
||
expect(TestHelper.getDocumentType('xlsx'), 'cell');
|
||
expect(TestHelper.getDocumentType('xlsm'), 'cell');
|
||
expect(TestHelper.getDocumentType('csv'), 'cell');
|
||
expect(TestHelper.getDocumentType('ods'), 'cell');
|
||
});
|
||
|
||
test('文档类型识别 - PowerPoint 文档', () {
|
||
expect(TestHelper.getDocumentType('ppt'), 'slide');
|
||
expect(TestHelper.getDocumentType('pptx'), 'slide');
|
||
expect(TestHelper.getDocumentType('pptm'), 'slide');
|
||
expect(TestHelper.getDocumentType('odp'), 'slide');
|
||
});
|
||
|
||
test('文档类型识别 - 未知扩展名默认为 word', () {
|
||
expect(TestHelper.getDocumentType('unknown'), 'word');
|
||
expect(TestHelper.getDocumentType('xyz'), 'word');
|
||
expect(TestHelper.getDocumentType(''), 'word');
|
||
});
|
||
|
||
test('文档密钥生成 - SHA256 哈希', () {
|
||
final url1 = 'https://example.com/test.docx';
|
||
final url2 = 'https://example.com/test.docx';
|
||
final url3 = 'https://example.com/different.docx';
|
||
|
||
// 相同 URL 应该生成相同的密钥
|
||
expect(TestHelper.generateDocKey(url1), TestHelper.generateDocKey(url2));
|
||
|
||
// 不同 URL 应该生成不同的密钥
|
||
expect(TestHelper.generateDocKey(url1), isNot(TestHelper.generateDocKey(url3)));
|
||
|
||
// 验证密钥是 SHA256 哈希(64个字符的十六进制)
|
||
final key = TestHelper.generateDocKey(url1);
|
||
expect(key.length, 64);
|
||
expect(RegExp(r'^[a-f0-9]{64}$').hasMatch(key), true);
|
||
});
|
||
|
||
test('文档密钥生成 - 真实文件 URL', () {
|
||
const realFileUrl = 'https://quanxue-oa.oss-cn-chengdu.aliyuncs.com/20250815/1755244744547.pptx';
|
||
final key = TestHelper.generateDocKey(realFileUrl);
|
||
|
||
expect(key, isNotEmpty);
|
||
expect(key.length, 64);
|
||
expect(RegExp(r'^[a-f0-9]{64}$').hasMatch(key), true);
|
||
});
|
||
|
||
test('URL 标准化 - 移除尾部斜杠', () {
|
||
expect(TestHelper.normalizeUrl('https://document.23544.com/'), 'https://document.23544.com');
|
||
expect(TestHelper.normalizeUrl('https://document.23544.com'), 'https://document.23544.com');
|
||
expect(TestHelper.normalizeUrl('https://example.com///'), 'https://example.com//');
|
||
});
|
||
|
||
test('URL 标准化 - 去除首尾空格', () {
|
||
expect(TestHelper.normalizeUrl(' https://document.23544.com/ '), 'https://document.23544.com');
|
||
expect(TestHelper.normalizeUrl('\nhttps://document.23544.com/\t'), 'https://document.23544.com');
|
||
expect(TestHelper.normalizeUrl(' https://example.com '), 'https://example.com');
|
||
});
|
||
|
||
test('配置生成 - 基本配置', () {
|
||
final config = TestHelper.createConfig(fileUrl: 'https://example.com/test.docx', jwtSecret: null);
|
||
|
||
expect(config['document'], isNotNull);
|
||
expect(config['document']['fileType'], 'docx');
|
||
expect(config['document']['url'], 'https://example.com/test.docx');
|
||
expect(config['document']['title'], 'test.docx');
|
||
expect(config['document']['key'], isNotNull);
|
||
expect(config['documentType'], 'word');
|
||
expect(config['editorConfig'], isNotNull);
|
||
expect(config['editorConfig']['mode'], 'view');
|
||
expect(config['editorConfig']['lang'], 'zh-CN');
|
||
expect(config['type'], 'mobile');
|
||
expect(config['token'], isNull);
|
||
});
|
||
|
||
test('配置生成 - 带 JWT secret 生成 token', () {
|
||
final config = TestHelper.createConfig(
|
||
fileUrl: 'https://quanxue-oa.oss-cn-chengdu.aliyuncs.com/20250815/1755244744547.pptx',
|
||
jwtSecret: '6Yr6DGoVV3ACS6GtVgdH453mXxLftd6Q',
|
||
);
|
||
|
||
// 验证 token 已生成
|
||
expect(config['token'], isNotNull);
|
||
expect(config['token'], isNotEmpty);
|
||
|
||
// 验证 token 是有效的 JWT 格式 (header.payload.signature)
|
||
final tokenParts = (config['token'] as String).split('.');
|
||
expect(tokenParts.length, 3);
|
||
|
||
expect(config['document']['fileType'], 'pptx');
|
||
expect(config['documentType'], 'slide');
|
||
});
|
||
|
||
test('配置生成 - 空 jwtSecret 不添加 token 到配置', () {
|
||
final config1 = TestHelper.createConfig(fileUrl: 'https://example.com/test.docx', jwtSecret: '');
|
||
expect(config1['token'], isNull);
|
||
|
||
final config2 = TestHelper.createConfig(fileUrl: 'https://example.com/test.docx', jwtSecret: null);
|
||
expect(config2['token'], isNull);
|
||
});
|
||
|
||
test('HTML 生成 - 包含必要元素', () {
|
||
final html = TestHelper.buildHtml(
|
||
serverUrl: 'https://document.23544.com/',
|
||
fileUrl: 'https://example.com/test.docx',
|
||
jwtSecret: null,
|
||
);
|
||
|
||
// 验证 HTML 结构
|
||
expect(html.contains('<!DOCTYPE html>'), true);
|
||
expect(html.contains('<html lang="en">'), true);
|
||
expect(html.contains('<meta charset="UTF-8">'), true);
|
||
expect(html.contains('<meta name="viewport"'), true);
|
||
expect(html.contains('<div id="placeholder"></div>'), true);
|
||
|
||
// 验证 API 脚本引用
|
||
expect(html.contains('https://document.23544.com/web-apps/apps/api/documents/api.js'), true);
|
||
|
||
// 验证 DocsAPI.DocEditor 调用
|
||
expect(html.contains('new DocsAPI.DocEditor("placeholder", config)'), true);
|
||
|
||
// 验证事件处理
|
||
expect(html.contains('"onAppReady"'), true);
|
||
expect(html.contains('"onDocumentReady"'), true);
|
||
expect(html.contains('"onError"'), true);
|
||
});
|
||
|
||
test('HTML 生成 - 真实数据配置', () {
|
||
final html = TestHelper.buildHtml(
|
||
serverUrl: 'https://document.23544.com',
|
||
fileUrl: 'https://quanxue-oa.oss-cn-chengdu.aliyuncs.com/20250815/1755244744547.pptx',
|
||
jwtSecret: '6Yr6DGoVV3ACS6GtVgdH453mXxLftd6Q',
|
||
);
|
||
|
||
// 验证配置包含 token (JWT 签名后的)
|
||
expect(html.contains('"token"'), true);
|
||
|
||
// 验证文件信息
|
||
expect(html.contains('1755244744547.pptx'), true);
|
||
expect(html.contains('"fileType":"pptx"'), true);
|
||
expect(html.contains('"documentType":"slide"'), true);
|
||
});
|
||
|
||
test('HTML 生成 - CSS 样式', () {
|
||
final html = TestHelper.buildHtml(
|
||
serverUrl: 'https://document.23544.com',
|
||
fileUrl: 'https://example.com/test.docx',
|
||
jwtSecret: null,
|
||
);
|
||
|
||
expect(html.contains('margin: 0; padding: 0; height: 100%; width: 100%; overflow: hidden;'), true);
|
||
expect(html.contains('#placeholder { height: 100%; }'), true);
|
||
});
|
||
|
||
test('真实服务器 URL 验证', () {
|
||
const realServerUrl = 'https://document.23544.com/';
|
||
final normalizedUrl = TestHelper.normalizeUrl(realServerUrl);
|
||
final expectedApiUrl = '$normalizedUrl/web-apps/apps/api/documents/api.js';
|
||
|
||
expect(normalizedUrl, 'https://document.23544.com');
|
||
expect(expectedApiUrl, 'https://document.23544.com/web-apps/apps/api/documents/api.js');
|
||
});
|
||
|
||
test('真实文件 URL 解析', () {
|
||
const realFileUrl = 'https://quanxue-oa.oss-cn-chengdu.aliyuncs.com/20250815/1755244744547.pptx';
|
||
final fileName = realFileUrl.split('/').last;
|
||
final fileExtension = fileName.split('.').last.toLowerCase();
|
||
|
||
expect(fileName, '1755244744547.pptx');
|
||
expect(fileExtension, 'pptx');
|
||
expect(TestHelper.getDocumentType(fileExtension), 'slide');
|
||
});
|
||
|
||
test('真实文件类型识别 - PPTX', () {
|
||
const realFileUrl = 'https://quanxue-oa.oss-cn-chengdu.aliyuncs.com/20250815/1755244744547.pptx';
|
||
final fileExtension = realFileUrl.split('.').last.toLowerCase();
|
||
final documentType = TestHelper.getDocumentType(fileExtension);
|
||
|
||
expect(documentType, 'slide');
|
||
});
|
||
|
||
test('JWT Secret 验证', () {
|
||
const realSecret = '6Yr6DGoVV3ACS6GtVgdH453mXxLftd6Q';
|
||
|
||
expect(realSecret, isNotNull);
|
||
expect(realSecret, isNotEmpty);
|
||
expect(realSecret.length, 32);
|
||
});
|
||
|
||
test('JWT Token 生成和验证', () {
|
||
const jwtSecret = '6Yr6DGoVV3ACS6GtVgdH453mXxLftd6Q';
|
||
const fileUrl = 'https://example.com/test.docx';
|
||
|
||
final config = TestHelper.createConfig(fileUrl: fileUrl, jwtSecret: jwtSecret);
|
||
|
||
expect(config['token'], isNotNull);
|
||
final token = config['token'] as String;
|
||
|
||
// 验证 token 格式
|
||
final tokenParts = token.split('.');
|
||
expect(tokenParts.length, 3); // header.payload.signature
|
||
|
||
// 验证 token 可以被解码和验证
|
||
final jwt = JWT.verify(token, SecretKey(jwtSecret));
|
||
expect(jwt.payload, isNotNull);
|
||
|
||
// 验证 payload 包含配置信息
|
||
final payload = jwt.payload as Map<String, dynamic>;
|
||
expect(payload['document'], isNotNull);
|
||
expect(payload['document']['key'], config['document']['key']);
|
||
expect(payload['documentType'], 'word');
|
||
});
|
||
|
||
test('文件 URL 格式验证', () {
|
||
const realFileUrl = 'https://quanxue-oa.oss-cn-chengdu.aliyuncs.com/20250815/1755244744547.pptx';
|
||
final uri = Uri.parse(realFileUrl);
|
||
|
||
expect(uri.scheme, 'https');
|
||
expect(uri.host, 'quanxue-oa.oss-cn-chengdu.aliyuncs.com');
|
||
expect(uri.path, '/20250815/1755244744547.pptx');
|
||
expect(uri.hasScheme, true);
|
||
expect(uri.hasAuthority, true);
|
||
});
|
||
});
|
||
}
|
||
|
||
/// 测试辅助类 - 复制 OnlyOfficeViewer 的内部逻辑用于测试
|
||
class TestHelper {
|
||
static String getDocumentType(String extension) {
|
||
const wordExtensions = [
|
||
'doc',
|
||
'docx',
|
||
'docm',
|
||
'dot',
|
||
'dotx',
|
||
'dotm',
|
||
'odt',
|
||
'fodt',
|
||
'ott',
|
||
'rtf',
|
||
'txt',
|
||
'html',
|
||
'htm',
|
||
'mht',
|
||
'pdf',
|
||
'djvu',
|
||
'fb2',
|
||
'epub',
|
||
'xps',
|
||
];
|
||
const cellExtensions = ['xls', 'xlsx', 'xlsm', 'xlt', 'xltx', 'xltm', 'ods', 'fods', 'ots', 'csv'];
|
||
const slideExtensions = ['ppt', 'pptx', 'pptm', 'pps', 'ppsx', 'ppsm', 'pot', 'potx', 'potm', 'odp', 'fodp', 'otp'];
|
||
|
||
if (wordExtensions.contains(extension)) return 'word';
|
||
if (cellExtensions.contains(extension)) return 'cell';
|
||
if (slideExtensions.contains(extension)) return 'slide';
|
||
return 'word';
|
||
}
|
||
|
||
static String generateDocKey(String url) {
|
||
return sha256.convert(utf8.encode(url)).toString();
|
||
}
|
||
|
||
static String normalizeUrl(String url) {
|
||
var trimmedUrl = url.trim();
|
||
if (trimmedUrl.endsWith('/')) {
|
||
return trimmedUrl.substring(0, trimmedUrl.length - 1);
|
||
}
|
||
return trimmedUrl;
|
||
}
|
||
|
||
static Map<String, dynamic> createConfig({required String fileUrl, required String? jwtSecret}) {
|
||
final fileExt = fileUrl.split('.').last.toLowerCase();
|
||
final documentType = getDocumentType(fileExt);
|
||
|
||
final config = {
|
||
'document': {
|
||
'fileType': fileExt,
|
||
'key': generateDocKey(fileUrl),
|
||
'title': fileUrl.split('/').last,
|
||
'url': fileUrl,
|
||
},
|
||
'documentType': documentType,
|
||
'editorConfig': {'mode': 'view', 'lang': 'zh-CN'},
|
||
'type': 'mobile',
|
||
};
|
||
|
||
// Sign the entire config with JWT if secret is provided
|
||
if (jwtSecret != null && jwtSecret.isNotEmpty) {
|
||
final jwt = JWT(config);
|
||
final token = jwt.sign(SecretKey(jwtSecret), algorithm: JWTAlgorithm.HS256);
|
||
config['token'] = token;
|
||
}
|
||
|
||
return config;
|
||
}
|
||
|
||
static String buildHtml({required String serverUrl, required String fileUrl, required String? jwtSecret}) {
|
||
final apiJsUrl = '${normalizeUrl(serverUrl)}/web-apps/apps/api/documents/api.js';
|
||
final config = createConfig(fileUrl: fileUrl, jwtSecret: jwtSecret);
|
||
final configJson = jsonEncode(config);
|
||
|
||
return '''
|
||
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||
<title>OnlyOffice Viewer</title>
|
||
<style>
|
||
html, body { margin: 0; padding: 0; height: 100%; width: 100%; overflow: hidden; }
|
||
#placeholder { height: 100%; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="placeholder"></div>
|
||
<script type="text/javascript" src="$apiJsUrl"></script>
|
||
<script type="text/javascript">
|
||
var config = $configJson;
|
||
|
||
config.events = {
|
||
"onAppReady": function() {
|
||
console.log('OnlyOffice App Ready');
|
||
},
|
||
"onDocumentReady": function() {
|
||
console.log('OnlyOffice Document Ready');
|
||
},
|
||
"onError": function(event) {
|
||
console.error('OnlyOffice Error:', event.data);
|
||
}
|
||
};
|
||
|
||
new DocsAPI.DocEditor("placeholder", config);
|
||
</script>
|
||
</body>
|
||
</html>
|
||
''';
|
||
}
|
||
}
|