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
''';
}
}