Merge feature/add_skin into master: resolve all conflicts
- GameGlobal.js: keep upstream SERVER_URL with /ws suffix - en.js/zh.js: merge both settings.nickname and settings.profile keys - SettingsScene.js: keep both nickname row and profile button - server/index.js: merge express app + content security proxy with noServer WebSocket mode and path validation - Add .gitignore for node_modules and .codebuddy
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user