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(''), true); expect(html.contains(''), true); expect(html.contains(''), true); expect(html.contains(''), 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; 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 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 ''' OnlyOffice Viewer
'''; } }