/** * contentSecurityManager.test.js * Unit tests for client-side ContentSecurityManager. * * Since ContentSecurityManager depends on wx APIs (wx.request, wx.getStorageSync, etc.), * we mock these APIs before requiring the module. */ const { describe, it, before, after, beforeEach } = require('node:test'); const assert = require('node:assert/strict'); // ============================================================ // Mock wx APIs // ============================================================ const mockStorage = {}; const mockRequests = {}; global.wx = { getStorageSync: (key) => mockStorage[key] || '', setStorageSync: (key, value) => { mockStorage[key] = value; }, request: (options) => { const key = `${options.method}:${options.url}`; if (mockRequests[key]) { const handler = mockRequests[key]; setTimeout(() => { if (handler.success) { options.success(handler.success); } else if (handler.fail) { options.fail(handler.fail); } }, 0); } else { // Default: 404 setTimeout(() => { options.fail({ errMsg: 'request:fail' }); }, 0); } }, uploadFile: (options) => { setTimeout(() => { options.fail({ errMsg: 'uploadFile:fail' }); }, 0); }, showToast: () => {}, showKeyboard: () => {}, hideKeyboard: () => {}, onKeyboardInput: () => {}, offKeyboardInput: () => {}, onKeyboardConfirm: () => {}, offKeyboardConfirm: () => {}, chooseImage: () => {}, getFileInfo: () => ({ size: 0 }), }; // Now require the module after mocking wx const ContentSecurityManager = require('../../js/managers/ContentSecurityManager'); // ============================================================ // Tests // ============================================================ describe('ContentSecurityManager - Local Check', () => { let manager; before(async () => { manager = new ContentSecurityManager(); await manager.init({ serverUrl: 'http://localhost:3000' }); }); it('should initialize with fallback word list', () => { assert.ok(manager.isInitialized()); assert.ok(manager.getWordCount() > 0); }); it('should detect sensitive words in content', () => { const result = manager.checkLocalText('这是一个赌博网站'); assert.strictEqual(result.hasViolation, true); assert.ok(result.matchedWords.length > 0); assert.ok(result.matchedWords.includes('赌博')); }); it('should detect political content', () => { const result = manager.checkLocalText('法轮功是邪教'); assert.strictEqual(result.hasViolation, true); assert.ok(result.matchedWords.includes('法轮功')); }); it('should detect pornographic content', () => { const result = manager.checkLocalText('色情视频'); assert.strictEqual(result.hasViolation, true); assert.ok(result.matchedWords.includes('色情')); }); it('should detect abusive language', () => { const result = manager.checkLocalText('你是个傻逼'); assert.strictEqual(result.hasViolation, true); assert.ok(result.matchedWords.includes('傻逼')); }); it('should pass clean content', () => { const result = manager.checkLocalText('今天天气真好,适合出去玩'); assert.strictEqual(result.hasViolation, false); assert.strictEqual(result.matchedWords.length, 0); }); it('should handle empty content', () => { const result = manager.checkLocalText(''); assert.strictEqual(result.hasViolation, false); }); it('should handle null content', () => { const result = manager.checkLocalText(null); assert.strictEqual(result.hasViolation, false); }); it('should complete within 50ms', () => { const start = Date.now(); for (let i = 0; i < 100; i++) { manager.checkLocalText('这是一个普通的测试内容,不包含任何敏感词汇'); } const duration = Date.now() - start; const avgMs = duration / 100; assert.ok(avgMs < 50, `Average check time ${avgMs}ms exceeds 50ms target`); }); it('should be case-insensitive', () => { const result = manager.checkLocalText('赌博'); assert.strictEqual(result.hasViolation, true); }); }); describe('ContentSecurityManager - Word List Cache', () => { let manager; before(async () => { // Clear cache Object.keys(mockStorage).forEach(k => delete mockStorage[k]); manager = new ContentSecurityManager(); await manager.init({ serverUrl: 'http://localhost:3000' }); }); it('should cache word list locally', () => { // After init, cache should be populated assert.ok(mockStorage['content_security_word_cache'] || manager.getWordCount() > 0); }); it('should load from cache on subsequent init', async () => { // Pre-populate cache mockStorage['content_security_word_timestamp'] = Date.now(); mockStorage['content_security_word_version'] = 'test_v1'; mockStorage['content_security_word_cache'] = ['测试词1', '测试词2']; const manager2 = new ContentSecurityManager(); await manager2.init({ serverUrl: 'http://localhost:3000' }); assert.strictEqual(manager2.getWordCount(), 2); }); it('should ignore expired cache', async () => { // Set expired cache mockStorage['content_security_word_timestamp'] = Date.now() - 25 * 60 * 60 * 1000; // 25 hours ago mockStorage['content_security_word_version'] = 'expired_v1'; mockStorage['content_security_word_cache'] = ['过期词1']; const manager3 = new ContentSecurityManager(); await manager3.init({ serverUrl: 'http://localhost:3000' }); // Should use fallback, not expired cache assert.ok(manager3.getWordCount() > 1); }); }); describe('ContentSecurityManager - Full Text Check', () => { let manager; before(async () => { Object.keys(mockStorage).forEach(k => delete mockStorage[k]); manager = new ContentSecurityManager(); await manager.init({ serverUrl: 'http://localhost:3000' }); }); it('should reject content with local violations', async () => { const result = await manager.fullTextCheck('test_openid', '赌博网站', ContentSecurityManager.SCENE.CHAT); assert.strictEqual(result.pass, false); assert.strictEqual(result.localViolation, true); assert.ok(result.errorMessage.length > 0); }); it('should provide appropriate error message for nickname scene', async () => { const result = await manager.fullTextCheck('test_openid', '赌博', ContentSecurityManager.SCENE.NICKNAME); assert.ok(result.errorMessage.includes('昵称')); }); it('should provide appropriate error message for chat scene', async () => { const result = await manager.fullTextCheck('test_openid', '色情内容', ContentSecurityManager.SCENE.CHAT); assert.ok(result.errorMessage.includes('违规')); }); it('should provide appropriate error message for signature scene', async () => { const result = await manager.fullTextCheck('test_openid', '色情', ContentSecurityManager.SCENE.SIGNATURE); assert.ok(result.errorMessage.includes('违规')); }); }); describe('ContentSecurityManager - SCENE Constants', () => { it('should export SCENE constants', () => { assert.strictEqual(ContentSecurityManager.SCENE.NICKNAME, 1); assert.strictEqual(ContentSecurityManager.SCENE.CHAT, 2); assert.strictEqual(ContentSecurityManager.SCENE.SIGNATURE, 3); assert.strictEqual(ContentSecurityManager.SCENE.DESCRIPTION, 4); }); it('should export REPORT_REASONS constants', () => { assert.strictEqual(ContentSecurityManager.REPORT_REASONS.POLITICS, 'politics'); assert.strictEqual(ContentSecurityManager.REPORT_REASONS.PORNOGRAPHY, 'pornography'); assert.strictEqual(ContentSecurityManager.REPORT_REASONS.GAMBLING, 'gambling'); assert.strictEqual(ContentSecurityManager.REPORT_REASONS.OTHER, 'other'); }); }); describe('ContentSecurityManager - Mute Status', () => { let manager; before(async () => { Object.keys(mockStorage).forEach(k => delete mockStorage[k]); manager = new ContentSecurityManager(); await manager.init({ serverUrl: 'http://localhost:3000' }); }); it('should return not muted when server is unreachable', async () => { const status = await manager.getMuteStatus('nonexistent_user'); assert.strictEqual(status.isMuted, false); assert.strictEqual(status.remainingMs, 0); }); }); describe('ContentSecurityManager - Submit Report', () => { let manager; before(async () => { Object.keys(mockStorage).forEach(k => delete mockStorage[k]); manager = new ContentSecurityManager(); await manager.init({ serverUrl: 'http://localhost:3000' }); }); it('should handle report submission failure gracefully', async () => { const result = await manager.submitReport({ contentId: 'test_123', targetUserId: 'target_456', contentType: 'chat', contentSummary: 'test', reporterId: 'reporter_789', reason: 'politics', }); // Should not throw, returns failure object assert.strictEqual(result.success, false); }); });