/** * 状态一致性测试套件 * * 测试范围: * 1. Token 过期处理和刷新机制 * 2. 存储切换后数据一致性 * 3. 会话状态管理 * 4. 本地存储状态恢复 */ const assert = require('assert'); // 测试结果收集器 const testResults = { passed: 0, failed: 0, errors: [] }; // 测试辅助函数 function test(name, fn) { try { fn(); testResults.passed++; console.log(` [PASS] ${name}`); } catch (error) { testResults.failed++; testResults.errors.push({ name, error: error.message }); console.log(` [FAIL] ${name}: ${error.message}`); } } async function asyncTest(name, fn) { try { await fn(); testResults.passed++; console.log(` [PASS] ${name}`); } catch (error) { testResults.failed++; testResults.errors.push({ name, error: error.message }); console.log(` [FAIL] ${name}: ${error.message}`); } } async function runTests() { // ============================================================ // 1. Token 管理测试 // ============================================================ console.log('\n========== 1. Token 管理测试 ==========\n'); function testTokenManagement() { console.log('--- 测试 Token 过期和刷新机制 ---'); // 模拟 JWT Token 结构 function createMockToken(payload, expiresInMs) { const header = { alg: 'HS256', typ: 'JWT' }; const iat = Math.floor(Date.now() / 1000); const exp = iat + Math.floor(expiresInMs / 1000); const tokenPayload = { ...payload, iat, exp }; // 简化的 base64 编码(仅用于测试) const base64Header = Buffer.from(JSON.stringify(header)).toString('base64url'); const base64Payload = Buffer.from(JSON.stringify(tokenPayload)).toString('base64url'); return `${base64Header}.${base64Payload}.signature`; } // 解析 Token 并检查过期 function parseToken(token) { try { const parts = token.split('.'); if (parts.length !== 3) return null; const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString()); const now = Math.floor(Date.now() / 1000); return { ...payload, isExpired: payload.exp < now, expiresIn: (payload.exp - now) * 1000 }; } catch { return null; } } // 检查是否需要刷新 Token(提前 5 分钟刷新) function needsRefresh(token, thresholdMs = 5 * 60 * 1000) { const parsed = parseToken(token); if (!parsed) return true; return parsed.expiresIn < thresholdMs; } test('有效 Token 应该能正确解析', () => { const token = createMockToken({ id: 1, username: 'test' }, 2 * 60 * 60 * 1000); const parsed = parseToken(token); assert.ok(parsed, 'Token 应该能被解析'); assert.strictEqual(parsed.id, 1); assert.strictEqual(parsed.username, 'test'); assert.strictEqual(parsed.isExpired, false); }); test('过期 Token 应该被正确识别', () => { const token = createMockToken({ id: 1 }, -1000); // 已过期 const parsed = parseToken(token); assert.ok(parsed.isExpired, 'Token 应该被标记为过期'); }); test('即将过期的 Token 应该触发刷新', () => { const token = createMockToken({ id: 1 }, 3 * 60 * 1000); // 3 分钟后过期 assert.ok(needsRefresh(token, 5 * 60 * 1000), '3 分钟后过期的 Token 应该触发刷新'); }); test('有效期充足的 Token 不应该触发刷新', () => { const token = createMockToken({ id: 1 }, 30 * 60 * 1000); // 30 分钟后过期 assert.ok(!needsRefresh(token, 5 * 60 * 1000), '30 分钟后过期的 Token 不应该触发刷新'); }); test('无效 Token 格式应该返回 null', () => { assert.strictEqual(parseToken('invalid'), null); assert.strictEqual(parseToken('a.b'), null); assert.strictEqual(parseToken(''), null); }); } testTokenManagement(); // ============================================================ // 2. 存储切换一致性测试 // ============================================================ console.log('\n========== 2. 存储切换一致性测试 ==========\n'); function testStorageSwitchConsistency() { console.log('--- 测试存储类型切换数据一致性 ---'); // 模拟用户存储状态 class UserStorageState { constructor(user) { this.userId = user.id; this.storageType = user.current_storage_type || 'oss'; this.permission = user.storage_permission || 'oss_only'; this.localQuota = user.local_storage_quota || 1073741824; this.localUsed = user.local_storage_used || 0; this.hasOssConfig = user.has_oss_config || 0; } // 检查是否可以切换到指定存储类型 canSwitchTo(targetType) { // 检查权限 if (this.permission === 'oss_only' && targetType === 'local') { return { allowed: false, reason: '您没有使用本地存储的权限' }; } if (this.permission === 'local_only' && targetType === 'oss') { return { allowed: false, reason: '您没有使用 OSS 存储的权限' }; } // 检查 OSS 配置 if (targetType === 'oss' && !this.hasOssConfig) { return { allowed: false, reason: '请先配置 OSS 服务' }; } // 检查本地存储配额 if (targetType === 'local' && this.localUsed >= this.localQuota) { return { allowed: false, reason: '本地存储空间已满' }; } return { allowed: true }; } // 切换存储类型 switchTo(targetType) { const check = this.canSwitchTo(targetType); if (!check.allowed) { throw new Error(check.reason); } this.storageType = targetType; return true; } // 获取当前可用空间 getAvailableSpace() { if (this.storageType === 'local') { return this.localQuota - this.localUsed; } return null; // OSS 空间由用户 Bucket 决定 } } test('OSS only 权限用户不能切换到本地存储', () => { const user = { id: 1, storage_permission: 'oss_only', has_oss_config: 1 }; const state = new UserStorageState(user); const result = state.canSwitchTo('local'); assert.strictEqual(result.allowed, false); assert.ok(result.reason.includes('权限')); }); test('本地 only 权限用户不能切换到 OSS 存储', () => { const user = { id: 1, storage_permission: 'local_only' }; const state = new UserStorageState(user); const result = state.canSwitchTo('oss'); assert.strictEqual(result.allowed, false); assert.ok(result.reason.includes('权限')); }); test('未配置 OSS 的用户不能切换到 OSS', () => { const user = { id: 1, storage_permission: 'both', has_oss_config: 0 }; const state = new UserStorageState(user); const result = state.canSwitchTo('oss'); assert.strictEqual(result.allowed, false); assert.ok(result.reason.includes('配置')); }); test('本地存储已满时不能切换到本地', () => { const user = { id: 1, storage_permission: 'both', local_storage_quota: 1000, local_storage_used: 1000 }; const state = new UserStorageState(user); const result = state.canSwitchTo('local'); assert.strictEqual(result.allowed, false); assert.ok(result.reason.includes('已满')); }); test('有权限且已配置的用户可以自由切换', () => { const user = { id: 1, storage_permission: 'both', has_oss_config: 1, local_storage_quota: 10000, local_storage_used: 5000 }; const state = new UserStorageState(user); assert.ok(state.canSwitchTo('oss').allowed); assert.ok(state.canSwitchTo('local').allowed); }); test('切换后状态应该正确更新', () => { const user = { id: 1, storage_permission: 'both', has_oss_config: 1, current_storage_type: 'oss' }; const state = new UserStorageState(user); assert.strictEqual(state.storageType, 'oss'); state.switchTo('local'); assert.strictEqual(state.storageType, 'local'); }); } testStorageSwitchConsistency(); // ============================================================ // 3. 会话状态管理测试 // ============================================================ console.log('\n========== 3. 会话状态管理测试 ==========\n'); async function testSessionManagement() { console.log('--- 测试会话状态管理 ---'); // 模拟会话管理器 class SessionManager { constructor() { this.sessions = new Map(); this.sessionTTL = 30 * 60 * 1000; // 30 分钟 } createSession(userId) { const sessionId = `sess_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const session = { id: sessionId, userId, createdAt: Date.now(), lastActivity: Date.now(), data: {} }; this.sessions.set(sessionId, session); return sessionId; } getSession(sessionId) { const session = this.sessions.get(sessionId); if (!session) return null; // 检查会话是否过期 if (Date.now() - session.lastActivity > this.sessionTTL) { this.sessions.delete(sessionId); return null; } // 更新最后活动时间 session.lastActivity = Date.now(); return session; } updateSessionData(sessionId, data) { const session = this.getSession(sessionId); if (!session) return false; session.data = { ...session.data, ...data }; return true; } destroySession(sessionId) { return this.sessions.delete(sessionId); } getActiveSessions(userId) { const now = Date.now(); const active = []; for (const session of this.sessions.values()) { if (session.userId === userId && now - session.lastActivity <= this.sessionTTL) { active.push(session); } } return active; } // 强制登出用户所有会话 destroyUserSessions(userId) { let count = 0; for (const [sessionId, session] of this.sessions.entries()) { if (session.userId === userId) { this.sessions.delete(sessionId); count++; } } return count; } } const manager = new SessionManager(); await asyncTest('创建会话应该返回有效的会话 ID', async () => { const sessionId = manager.createSession(1); assert.ok(sessionId.startsWith('sess_')); assert.ok(manager.getSession(sessionId) !== null); }); await asyncTest('获取会话应该返回正确的用户 ID', async () => { const sessionId = manager.createSession(42); const session = manager.getSession(sessionId); assert.strictEqual(session.userId, 42); }); await asyncTest('更新会话数据应该持久化', async () => { const sessionId = manager.createSession(1); manager.updateSessionData(sessionId, { captcha: 'ABC123' }); const session = manager.getSession(sessionId); assert.strictEqual(session.data.captcha, 'ABC123'); }); await asyncTest('销毁会话后应该无法获取', async () => { const sessionId = manager.createSession(1); manager.destroySession(sessionId); assert.strictEqual(manager.getSession(sessionId), null); }); await asyncTest('过期会话应该被自动清理', async () => { const shortTTLManager = new SessionManager(); shortTTLManager.sessionTTL = 10; // 10ms const sessionId = shortTTLManager.createSession(1); await new Promise(resolve => setTimeout(resolve, 20)); assert.strictEqual(shortTTLManager.getSession(sessionId), null); }); await asyncTest('强制登出应该清除用户所有会话', async () => { const sessionId1 = manager.createSession(100); const sessionId2 = manager.createSession(100); const sessionId3 = manager.createSession(100); const count = manager.destroyUserSessions(100); assert.strictEqual(count, 3); assert.strictEqual(manager.getSession(sessionId1), null); assert.strictEqual(manager.getSession(sessionId2), null); assert.strictEqual(manager.getSession(sessionId3), null); }); } await testSessionManagement(); // ============================================================ // 4. 本地存储状态恢复测试 // ============================================================ console.log('\n========== 4. 本地存储状态恢复测试 ==========\n'); function testLocalStorageRecovery() { console.log('--- 测试本地存储状态恢复 ---'); // 模拟 localStorage class MockLocalStorage { constructor() { this.store = {}; } getItem(key) { return this.store[key] || null; } setItem(key, value) { this.store[key] = String(value); } removeItem(key) { delete this.store[key]; } clear() { this.store = {}; } } // 状态恢复管理器 class StateRecoveryManager { constructor(storage) { this.storage = storage; this.stateKey = 'app_state'; } // 保存状态 saveState(state) { try { const serialized = JSON.stringify({ ...state, savedAt: Date.now() }); this.storage.setItem(this.stateKey, serialized); return true; } catch (e) { console.error('保存状态失败:', e); return false; } } // 恢复状态 restoreState(maxAgeMs = 24 * 60 * 60 * 1000) { try { const serialized = this.storage.getItem(this.stateKey); if (!serialized) return null; const state = JSON.parse(serialized); // 检查状态是否过期 if (Date.now() - state.savedAt > maxAgeMs) { this.clearState(); return null; } // 移除元数据 delete state.savedAt; return state; } catch (e) { console.error('恢复状态失败:', e); return null; } } // 清除状态 clearState() { this.storage.removeItem(this.stateKey); } // 合并恢复的状态和默认状态 mergeWithDefaults(defaults) { const restored = this.restoreState(); if (!restored) return defaults; // 只恢复允许持久化的字段 const allowedFields = ['currentView', 'fileViewMode', 'adminTab', 'currentPath']; const merged = { ...defaults }; for (const field of allowedFields) { if (field in restored) { merged[field] = restored[field]; } } return merged; } } const storage = new MockLocalStorage(); const manager = new StateRecoveryManager(storage); test('保存和恢复状态应该正常工作', () => { const state = { currentView: 'files', currentPath: '/documents' }; manager.saveState(state); const restored = manager.restoreState(); assert.strictEqual(restored.currentView, 'files'); assert.strictEqual(restored.currentPath, '/documents'); }); test('空存储应该返回 null', () => { const emptyStorage = new MockLocalStorage(); const emptyManager = new StateRecoveryManager(emptyStorage); assert.strictEqual(emptyManager.restoreState(), null); }); test('过期状态应该被清除', () => { // 手动设置一个过期的状态 storage.setItem('app_state', JSON.stringify({ currentView: 'old', savedAt: Date.now() - 48 * 60 * 60 * 1000 // 48小时前 })); const restored = manager.restoreState(24 * 60 * 60 * 1000); assert.strictEqual(restored, null); }); test('清除状态后应该无法恢复', () => { manager.saveState({ test: 'value' }); manager.clearState(); assert.strictEqual(manager.restoreState(), null); }); test('合并默认值应该优先使用恢复的值', () => { manager.saveState({ currentView: 'shares', adminTab: 'users' }); const defaults = { currentView: 'files', fileViewMode: 'grid', adminTab: 'overview' }; const merged = manager.mergeWithDefaults(defaults); assert.strictEqual(merged.currentView, 'shares'); assert.strictEqual(merged.adminTab, 'users'); assert.strictEqual(merged.fileViewMode, 'grid'); // 默认值 }); test('损坏的 JSON 应该返回 null', () => { storage.setItem('app_state', 'not valid json{'); assert.strictEqual(manager.restoreState(), null); }); } testLocalStorageRecovery(); // ============================================================ // 5. 并发状态更新测试 // ============================================================ console.log('\n========== 5. 并发状态更新测试 ==========\n'); async function testConcurrentStateUpdates() { console.log('--- 测试并发状态更新 ---'); // 简单的状态管理器(带版本控制) class VersionedStateManager { constructor(initialState = {}) { this.state = { ...initialState }; this.version = 0; this.updateQueue = []; this.processing = false; } getState() { return { ...this.state }; } getVersion() { return this.version; } // 乐观锁更新 async updateWithVersion(expectedVersion, updates) { return new Promise((resolve, reject) => { this.updateQueue.push({ expectedVersion, updates, resolve, reject }); this.processQueue(); }); } // 强制更新(忽略版本) forceUpdate(updates) { this.state = { ...this.state, ...updates }; this.version++; return { success: true, version: this.version }; } async processQueue() { if (this.processing || this.updateQueue.length === 0) return; this.processing = true; while (this.updateQueue.length > 0) { const { expectedVersion, updates, resolve, reject } = this.updateQueue.shift(); if (expectedVersion !== this.version) { reject(new Error('版本冲突,请刷新后重试')); continue; } this.state = { ...this.state, ...updates }; this.version++; resolve({ success: true, version: this.version, state: this.getState() }); } this.processing = false; } } await asyncTest('顺序更新应该成功', async () => { const manager = new VersionedStateManager({ count: 0 }); await manager.updateWithVersion(0, { count: 1 }); await manager.updateWithVersion(1, { count: 2 }); assert.strictEqual(manager.getState().count, 2); assert.strictEqual(manager.getVersion(), 2); }); await asyncTest('版本冲突应该被检测', async () => { const manager = new VersionedStateManager({ count: 0 }); // 第一个更新成功 await manager.updateWithVersion(0, { count: 1 }); // 使用旧版本尝试更新应该失败 try { await manager.updateWithVersion(0, { count: 2 }); assert.fail('应该抛出版本冲突错误'); } catch (error) { assert.ok(error.message.includes('冲突')); } }); await asyncTest('强制更新应该忽略版本', async () => { const manager = new VersionedStateManager({ value: 'old' }); manager.forceUpdate({ value: 'new' }); assert.strictEqual(manager.getState().value, 'new'); }); await asyncTest('并发更新应该按顺序处理', async () => { const manager = new VersionedStateManager({ count: 0 }); // 模拟并发更新 const results = await Promise.allSettled([ manager.updateWithVersion(0, { count: 1 }), manager.updateWithVersion(0, { count: 2 }), // 这个会失败 manager.updateWithVersion(0, { count: 3 }) // 这个也会失败 ]); const fulfilled = results.filter(r => r.status === 'fulfilled').length; const rejected = results.filter(r => r.status === 'rejected').length; assert.strictEqual(fulfilled, 1, '应该只有一个更新成功'); assert.strictEqual(rejected, 2, '应该有两个更新失败'); }); } await testConcurrentStateUpdates(); // ============================================================ // 6. 视图切换状态测试 // ============================================================ console.log('\n========== 6. 视图切换状态测试 ==========\n'); function testViewSwitchState() { console.log('--- 测试视图切换状态保持 ---'); // 视图状态管理器 class ViewStateManager { constructor() { this.currentView = 'files'; this.viewStates = { files: { path: '/', viewMode: 'grid', selection: [] }, shares: { viewMode: 'list', filter: 'all' }, admin: { tab: 'overview' } }; } switchTo(view) { if (!this.viewStates[view]) { throw new Error(`未知视图: ${view}`); } this.currentView = view; return this.getViewState(view); } getViewState(view) { return { ...this.viewStates[view || this.currentView] }; } updateViewState(view, updates) { if (!this.viewStates[view]) { throw new Error(`未知视图: ${view}`); } this.viewStates[view] = { ...this.viewStates[view], ...updates }; } // 获取完整状态快照 getSnapshot() { return { currentView: this.currentView, viewStates: JSON.parse(JSON.stringify(this.viewStates)) }; } // 从快照恢复 restoreFromSnapshot(snapshot) { this.currentView = snapshot.currentView; this.viewStates = JSON.parse(JSON.stringify(snapshot.viewStates)); } } const manager = new ViewStateManager(); test('切换视图应该返回该视图的状态', () => { const state = manager.switchTo('shares'); assert.strictEqual(state.viewMode, 'list'); assert.strictEqual(state.filter, 'all'); }); test('更新视图状态应该被保存', () => { manager.updateViewState('files', { path: '/documents', selection: ['file1.txt'] }); const state = manager.getViewState('files'); assert.strictEqual(state.path, '/documents'); assert.strictEqual(state.selection.length, 1); }); test('切换视图后再切换回来应该保留状态', () => { manager.updateViewState('files', { path: '/photos' }); manager.switchTo('shares'); manager.switchTo('files'); const state = manager.getViewState('files'); assert.strictEqual(state.path, '/photos'); }); test('切换到未知视图应该抛出错误', () => { assert.throws(() => manager.switchTo('unknown'), /未知视图/); }); test('快照和恢复应该正常工作', () => { manager.updateViewState('files', { path: '/backup' }); const snapshot = manager.getSnapshot(); // 修改状态 manager.updateViewState('files', { path: '/different' }); // 从快照恢复 manager.restoreFromSnapshot(snapshot); const state = manager.getViewState('files'); assert.strictEqual(state.path, '/backup'); }); } testViewSwitchState(); // ============================================================ // 7. 主题切换一致性测试 // ============================================================ console.log('\n========== 7. 主题切换一致性测试 ==========\n'); function testThemeConsistency() { console.log('--- 测试主题切换一致性 ---'); // 主题管理器 class ThemeManager { constructor(globalDefault = 'dark') { this.globalTheme = globalDefault; this.userTheme = null; // null 表示跟随全局 } setGlobalTheme(theme) { if (!['dark', 'light'].includes(theme)) { throw new Error('无效的主题'); } this.globalTheme = theme; } setUserTheme(theme) { if (theme !== null && !['dark', 'light'].includes(theme)) { throw new Error('无效的主题'); } this.userTheme = theme; } getEffectiveTheme() { return this.userTheme || this.globalTheme; } isFollowingGlobal() { return this.userTheme === null; } getThemeInfo() { return { global: this.globalTheme, user: this.userTheme, effective: this.getEffectiveTheme(), followingGlobal: this.isFollowingGlobal() }; } } test('默认应该使用全局主题', () => { const manager = new ThemeManager('dark'); assert.strictEqual(manager.getEffectiveTheme(), 'dark'); assert.ok(manager.isFollowingGlobal()); }); test('用户主题应该覆盖全局主题', () => { const manager = new ThemeManager('dark'); manager.setUserTheme('light'); assert.strictEqual(manager.getEffectiveTheme(), 'light'); assert.ok(!manager.isFollowingGlobal()); }); test('用户主题设为 null 应该跟随全局', () => { const manager = new ThemeManager('dark'); manager.setUserTheme('light'); manager.setUserTheme(null); assert.strictEqual(manager.getEffectiveTheme(), 'dark'); assert.ok(manager.isFollowingGlobal()); }); test('全局主题改变应该影响跟随全局的用户', () => { const manager = new ThemeManager('dark'); manager.setGlobalTheme('light'); assert.strictEqual(manager.getEffectiveTheme(), 'light'); }); test('全局主题改变不应该影响有自定义主题的用户', () => { const manager = new ThemeManager('dark'); manager.setUserTheme('light'); manager.setGlobalTheme('dark'); assert.strictEqual(manager.getEffectiveTheme(), 'light'); }); test('无效主题应该抛出错误', () => { const manager = new ThemeManager(); assert.throws(() => manager.setGlobalTheme('invalid'), /无效/); assert.throws(() => manager.setUserTheme('invalid'), /无效/); }); } testThemeConsistency(); // ============================================================ // 测试总结 // ============================================================ console.log('\n========================================'); console.log('测试总结'); console.log('========================================'); console.log(`通过: ${testResults.passed}`); console.log(`失败: ${testResults.failed}`); if (testResults.errors.length > 0) { console.log('\n失败的测试:'); testResults.errors.forEach((e, i) => { console.log(` ${i + 1}. ${e.name}: ${e.error}`); }); } console.log('\n'); return testResults; } // 运行测试 runTests().then(testResults => { process.exit(testResults.failed > 0 ? 1 : 0); }).catch(err => { console.error('测试执行错误:', err); process.exit(1); });