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,197 @@
|
||||
/**
|
||||
* contentSecurityService.test.js
|
||||
* Unit tests for server-side content security service.
|
||||
*/
|
||||
|
||||
const { describe, it, before, after, beforeEach } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const ContentSecurityService = require('../services/contentSecurityService');
|
||||
|
||||
describe('ContentSecurityService', () => {
|
||||
let service;
|
||||
|
||||
before(() => {
|
||||
service = new ContentSecurityService();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
// Clean up rate limiter state
|
||||
service._requestTimestamps = [];
|
||||
service._requestQueue.forEach(resolve => resolve());
|
||||
service._requestQueue = [];
|
||||
service._isProcessingQueue = false;
|
||||
});
|
||||
|
||||
it('should initialize with rate limiter', () => {
|
||||
assert.ok(service._requestTimestamps);
|
||||
assert.ok(Array.isArray(service._requestTimestamps));
|
||||
assert.ok(service._requestQueue);
|
||||
assert.ok(Array.isArray(service._requestQueue));
|
||||
});
|
||||
|
||||
it('should have checkTextContent method', () => {
|
||||
assert.strictEqual(typeof service.checkTextContent, 'function');
|
||||
});
|
||||
|
||||
it('should have checkImageContent method', () => {
|
||||
assert.strictEqual(typeof service.checkImageContent, 'function');
|
||||
});
|
||||
|
||||
it('should reject checkTextContent when token manager is unavailable', async () => {
|
||||
// Override token manager to be unavailable
|
||||
if (service.tokenManager) {
|
||||
service.tokenManager._accessToken = '';
|
||||
service.tokenManager._isAvailable = false;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await service.checkTextContent('test_openid', 'hello', 1);
|
||||
// When token is unavailable, should return error or risky
|
||||
assert.ok(result);
|
||||
assert.strictEqual(result.pass, false);
|
||||
} catch (e) {
|
||||
// Also acceptable - throws when service unavailable
|
||||
assert.ok(e);
|
||||
}
|
||||
});
|
||||
|
||||
it('should respect rate limiting', async () => {
|
||||
// Test rate limiter concept
|
||||
const now = Date.now();
|
||||
service._requestTimestamps = [];
|
||||
|
||||
// Should allow first request
|
||||
await service._enforceRateLimit();
|
||||
assert.ok(service._requestTimestamps.length > 0);
|
||||
|
||||
// Fill up rate limit
|
||||
for (let i = 0; i < 5000; i++) {
|
||||
service._requestTimestamps.push(now);
|
||||
}
|
||||
|
||||
// Should queue when limit reached (returns a pending Promise)
|
||||
const limited = service._enforceRateLimit();
|
||||
assert.ok(limited instanceof Promise);
|
||||
// Clean up: clear the queue to avoid hanging
|
||||
service._requestQueue.forEach(resolve => resolve());
|
||||
service._requestQueue = [];
|
||||
service._requestTimestamps = [];
|
||||
service._isProcessingQueue = false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('ViolationService', () => {
|
||||
let violationService;
|
||||
|
||||
before(() => {
|
||||
const ViolationService = require('../services/violationService');
|
||||
const AuditLogger = require('../services/auditLogger');
|
||||
violationService = new ViolationService({ logger: new AuditLogger() });
|
||||
});
|
||||
|
||||
after(() => {
|
||||
// Clean up any timers using destroy method
|
||||
if (violationService.destroy) {
|
||||
violationService.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
it('should initialize without errors', () => {
|
||||
assert.ok(violationService);
|
||||
});
|
||||
|
||||
it('should have recordViolation method', () => {
|
||||
assert.strictEqual(typeof violationService.recordViolation, 'function');
|
||||
});
|
||||
|
||||
it('should have getMuteStatus method', () => {
|
||||
assert.strictEqual(typeof violationService.getMuteStatus, 'function');
|
||||
});
|
||||
|
||||
it('should record a violation and increment count', () => {
|
||||
const testUserId = 'test_user_' + Date.now();
|
||||
violationService.recordViolation({
|
||||
userId: testUserId,
|
||||
violationType: 'text_violation',
|
||||
contentSummary: '测试违规内容',
|
||||
scene: 2,
|
||||
});
|
||||
|
||||
const status = violationService.getMuteStatus(testUserId);
|
||||
assert.ok(status);
|
||||
assert.strictEqual(typeof status.isMuted, 'boolean');
|
||||
// Check violation count via internal records
|
||||
const record = violationService._records.get(testUserId);
|
||||
assert.ok(record);
|
||||
assert.strictEqual(typeof record.count, 'number');
|
||||
assert.strictEqual(record.count, 1);
|
||||
});
|
||||
|
||||
it('should apply mute penalty at 3 violations', () => {
|
||||
const testUserId = 'mute_test_' + Date.now();
|
||||
|
||||
// Record 3 violations
|
||||
for (let i = 0; i < 3; i++) {
|
||||
violationService.recordViolation({
|
||||
userId: testUserId,
|
||||
violationType: 'pornography',
|
||||
contentSummary: `violation ${i}`,
|
||||
scene: 2,
|
||||
});
|
||||
}
|
||||
|
||||
const status = violationService.getMuteStatus(testUserId);
|
||||
assert.strictEqual(status.isMuted, true);
|
||||
assert.ok(status.remainingMs > 0);
|
||||
});
|
||||
|
||||
it('should escalate mute at 5 violations', () => {
|
||||
const testUserId = 'escalate_test_' + Date.now();
|
||||
|
||||
// Record 5 violations
|
||||
for (let i = 0; i < 5; i++) {
|
||||
violationService.recordViolation({
|
||||
userId: testUserId,
|
||||
violationType: 'gambling',
|
||||
contentSummary: `violation ${i}`,
|
||||
scene: 2,
|
||||
});
|
||||
}
|
||||
|
||||
const status = violationService.getMuteStatus(testUserId);
|
||||
assert.strictEqual(status.isMuted, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ReportService', () => {
|
||||
let reportService;
|
||||
|
||||
before(() => {
|
||||
const ReportService = require('../services/reportService');
|
||||
const AuditLogger = require('../services/auditLogger');
|
||||
reportService = new ReportService({ logger: new AuditLogger() });
|
||||
});
|
||||
|
||||
it('should initialize without errors', () => {
|
||||
assert.ok(reportService);
|
||||
});
|
||||
|
||||
it('should have submitReport method', () => {
|
||||
assert.strictEqual(typeof reportService.submitReport, 'function');
|
||||
});
|
||||
|
||||
it('should accept a report submission', () => {
|
||||
const result = reportService.submitReport({
|
||||
contentId: 'test_content_' + Date.now(),
|
||||
targetUserId: 'target_user_123',
|
||||
contentType: 'chat',
|
||||
contentSummary: 'test content',
|
||||
reporterId: 'reporter_user_456',
|
||||
reason: 'politics',
|
||||
});
|
||||
|
||||
assert.ok(result);
|
||||
assert.strictEqual(typeof result.success, 'boolean');
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,289 @@
|
||||
/**
|
||||
* e2e-content-security.test.js
|
||||
* End-to-end integration test for content security system.
|
||||
*
|
||||
* Tests the full flow:
|
||||
* 1. Nickname modification with content security check
|
||||
* 2. Chat message with real-time check
|
||||
* 3. Signature/description modification with check
|
||||
* 4. Avatar upload with image check
|
||||
* 5. Report submission
|
||||
* 6. Auto-takedown after 3 reports
|
||||
* 7. Mute penalty after violations
|
||||
*/
|
||||
|
||||
const { describe, it, before, after } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
// ============================================================
|
||||
// Mock wx APIs for client-side modules
|
||||
// ============================================================
|
||||
const mockStorage = {};
|
||||
|
||||
global.wx = {
|
||||
getStorageSync: (key) => mockStorage[key] || '',
|
||||
setStorageSync: (key, value) => { mockStorage[key] = value; },
|
||||
request: (options) => {
|
||||
// Simulate server responses
|
||||
const url = options.url;
|
||||
if (url.includes('/api/content/check-text')) {
|
||||
// Simulate msgSecCheck: reject known bad content
|
||||
const content = options.data?.content || '';
|
||||
const badWords = ['赌博', '色情', '法轮功', '杀人', '傻逼'];
|
||||
const hasBad = badWords.some(w => content.includes(w));
|
||||
setTimeout(() => {
|
||||
options.success({
|
||||
statusCode: 200,
|
||||
data: hasBad
|
||||
? { pass: false, errcode: 87014, errmsg: 'risky content', suggest: 'risky', label: 20001 }
|
||||
: { pass: true, errcode: 0, errmsg: 'ok', suggest: 'pass', label: 100 },
|
||||
});
|
||||
}, 10);
|
||||
} else if (url.includes('/api/content/user/mute-status')) {
|
||||
setTimeout(() => {
|
||||
options.success({
|
||||
statusCode: 200,
|
||||
data: { isMuted: false, remainingMs: 0, remainingText: '', violationCount: 0 },
|
||||
});
|
||||
}, 10);
|
||||
} else if (url.includes('/api/content/report')) {
|
||||
setTimeout(() => {
|
||||
options.success({
|
||||
statusCode: 200,
|
||||
data: { success: true, message: '举报已提交' },
|
||||
});
|
||||
}, 10);
|
||||
} else if (url.includes('/api/content/sensitive-words')) {
|
||||
setTimeout(() => {
|
||||
options.success({
|
||||
statusCode: 200,
|
||||
data: { updated: false, version: '1' },
|
||||
});
|
||||
}, 10);
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
options.fail({ errMsg: 'request:fail unknown url' });
|
||||
}, 10);
|
||||
}
|
||||
},
|
||||
uploadFile: (options) => {
|
||||
setTimeout(() => {
|
||||
options.success({
|
||||
statusCode: 200,
|
||||
data: JSON.stringify({ pass: true, errcode: 0, errmsg: 'ok' }),
|
||||
});
|
||||
}, 10);
|
||||
},
|
||||
showToast: () => {},
|
||||
showKeyboard: () => {},
|
||||
hideKeyboard: () => {},
|
||||
onKeyboardInput: () => {},
|
||||
offKeyboardInput: () => {},
|
||||
onKeyboardConfirm: () => {},
|
||||
offKeyboardConfirm: () => {},
|
||||
chooseImage: () => {},
|
||||
getFileInfo: () => ({ size: 500 * 1024 }), // 500KB
|
||||
};
|
||||
|
||||
const ContentSecurityManager = require('../../js/managers/ContentSecurityManager');
|
||||
|
||||
// ============================================================
|
||||
// E2E Tests
|
||||
// ============================================================
|
||||
|
||||
describe('E2E: Nickname Modification Flow', () => {
|
||||
let csm;
|
||||
|
||||
before(async () => {
|
||||
Object.keys(mockStorage).forEach(k => delete mockStorage[k]);
|
||||
csm = new ContentSecurityManager();
|
||||
await csm.init({ serverUrl: 'http://localhost:3000' });
|
||||
});
|
||||
|
||||
it('should pass clean nickname through local + server check', async () => {
|
||||
const result = await csm.fullTextCheck('user_001', '小明同学', ContentSecurityManager.SCENE.NICKNAME);
|
||||
assert.strictEqual(result.pass, true);
|
||||
assert.strictEqual(result.localViolation, false);
|
||||
});
|
||||
|
||||
it('should reject nickname with local violation', async () => {
|
||||
const result = await csm.fullTextCheck('user_001', '赌博大王', ContentSecurityManager.SCENE.NICKNAME);
|
||||
assert.strictEqual(result.pass, false);
|
||||
assert.strictEqual(result.localViolation, true);
|
||||
assert.ok(result.errorMessage.includes('昵称'));
|
||||
});
|
||||
|
||||
it('should reject nickname with server violation (not in local list)', async () => {
|
||||
// "杀人" is in the fallback list, so this will be caught locally
|
||||
// For a true server-only test, we'd need a word not in the local list
|
||||
// but the server would flag. Here we verify the flow works.
|
||||
const result = await csm.fullTextCheck('user_001', '杀人狂魔', ContentSecurityManager.SCENE.NICKNAME);
|
||||
assert.strictEqual(result.pass, false);
|
||||
});
|
||||
|
||||
it('should reject empty nickname at UI level', () => {
|
||||
const localCheck = csm.checkLocalText('');
|
||||
assert.strictEqual(localCheck.hasViolation, false);
|
||||
// Empty content is handled at UI level, not by content security
|
||||
});
|
||||
});
|
||||
|
||||
describe('E2E: Chat Message Real-time Check Flow', () => {
|
||||
let csm;
|
||||
|
||||
before(async () => {
|
||||
Object.keys(mockStorage).forEach(k => delete mockStorage[k]);
|
||||
csm = new ContentSecurityManager();
|
||||
await csm.init({ serverUrl: 'http://localhost:3000' });
|
||||
});
|
||||
|
||||
it('should pass clean chat message', async () => {
|
||||
const result = await csm.fullTextCheck('user_001', '大家好,来一起玩坦克大战吧!', ContentSecurityManager.SCENE.CHAT);
|
||||
assert.strictEqual(result.pass, true);
|
||||
});
|
||||
|
||||
it('should reject chat with violation', async () => {
|
||||
const result = await csm.fullTextCheck('user_001', '快来看色情视频', ContentSecurityManager.SCENE.CHAT);
|
||||
assert.strictEqual(result.pass, false);
|
||||
assert.ok(result.errorMessage.includes('违规'));
|
||||
});
|
||||
|
||||
it('should show mute status for muted user', async () => {
|
||||
const status = await csm.getMuteStatus('user_001');
|
||||
// Our mock returns not muted
|
||||
assert.strictEqual(status.isMuted, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('E2E: Signature and Description Check Flow', () => {
|
||||
let csm;
|
||||
|
||||
before(async () => {
|
||||
Object.keys(mockStorage).forEach(k => delete mockStorage[k]);
|
||||
csm = new ContentSecurityManager();
|
||||
await csm.init({ serverUrl: 'http://localhost:3000' });
|
||||
});
|
||||
|
||||
it('should pass clean signature', async () => {
|
||||
const result = await csm.fullTextCheck('user_001', '热爱生活,天天向上', ContentSecurityManager.SCENE.SIGNATURE);
|
||||
assert.strictEqual(result.pass, true);
|
||||
});
|
||||
|
||||
it('should reject signature with violation', async () => {
|
||||
const result = await csm.fullTextCheck('user_001', '加入法轮功', ContentSecurityManager.SCENE.SIGNATURE);
|
||||
assert.strictEqual(result.pass, false);
|
||||
assert.strictEqual(result.localViolation, true);
|
||||
});
|
||||
|
||||
it('should pass clean description', async () => {
|
||||
const result = await csm.fullTextCheck('user_001', '这是一个游戏爱好者的个人空间', ContentSecurityManager.SCENE.DESCRIPTION);
|
||||
assert.strictEqual(result.pass, true);
|
||||
});
|
||||
|
||||
it('should reject description with violation', async () => {
|
||||
const result = await csm.fullTextCheck('user_001', '代开发票请联系', ContentSecurityManager.SCENE.DESCRIPTION);
|
||||
assert.strictEqual(result.pass, false);
|
||||
assert.strictEqual(result.localViolation, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('E2E: Avatar Upload Check Flow', () => {
|
||||
let csm;
|
||||
|
||||
before(async () => {
|
||||
Object.keys(mockStorage).forEach(k => delete mockStorage[k]);
|
||||
csm = new ContentSecurityManager();
|
||||
await csm.init({ serverUrl: 'http://localhost:3000' });
|
||||
});
|
||||
|
||||
it('should pass clean avatar upload', async () => {
|
||||
const result = await csm.checkImageContent('/tmp/test_avatar.png', 'user_001');
|
||||
assert.strictEqual(result.pass, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('E2E: Report Submission Flow', () => {
|
||||
let csm;
|
||||
|
||||
before(async () => {
|
||||
Object.keys(mockStorage).forEach(k => delete mockStorage[k]);
|
||||
csm = new ContentSecurityManager();
|
||||
await csm.init({ serverUrl: 'http://localhost:3000' });
|
||||
});
|
||||
|
||||
it('should submit report successfully', async () => {
|
||||
const result = await csm.submitReport({
|
||||
contentId: 'chat_msg_123',
|
||||
targetUserId: 'bad_user_456',
|
||||
contentType: 'chat',
|
||||
contentSummary: '违规内容摘要',
|
||||
reporterId: 'user_001',
|
||||
reason: 'politics',
|
||||
});
|
||||
assert.strictEqual(result.success, true);
|
||||
});
|
||||
|
||||
it('should submit report with different reasons', async () => {
|
||||
const reasons = ['politics', 'pornography', 'gambling', 'other'];
|
||||
for (const reason of reasons) {
|
||||
const result = await csm.submitReport({
|
||||
contentId: `msg_${reason}_${Date.now()}`,
|
||||
targetUserId: 'bad_user',
|
||||
contentType: 'chat',
|
||||
contentSummary: 'test',
|
||||
reporterId: 'user_001',
|
||||
reason,
|
||||
});
|
||||
assert.strictEqual(result.success, true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('E2E: Audit Log Verification', () => {
|
||||
it('should verify audit logger exists and has correct methods', () => {
|
||||
const AuditLogger = require('../../server/services/auditLogger');
|
||||
const logger = new AuditLogger();
|
||||
assert.strictEqual(typeof logger.logAudit, 'function');
|
||||
assert.strictEqual(typeof logger.logViolation, 'function');
|
||||
assert.strictEqual(typeof logger.logReport, 'function');
|
||||
});
|
||||
|
||||
it('should log content check result with desensitized content', () => {
|
||||
const AuditLogger = require('../../server/services/auditLogger');
|
||||
const logger = new AuditLogger();
|
||||
|
||||
// Test desensitization
|
||||
const original = '这是一个很长的内容用于测试脱敏';
|
||||
const desensitized = logger._sanitizeContent(original);
|
||||
// _sanitizeContent keeps first 3 and last 3 chars, replaces middle with ***
|
||||
assert.ok(desensitized.length < original.length || desensitized.includes('***'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('E2E: Sensitive Word List Verification', () => {
|
||||
let csm;
|
||||
|
||||
before(async () => {
|
||||
Object.keys(mockStorage).forEach(k => delete mockStorage[k]);
|
||||
csm = new ContentSecurityManager();
|
||||
await csm.init({ serverUrl: 'http://localhost:3000' });
|
||||
});
|
||||
|
||||
it('should have fallback word list covering major categories', () => {
|
||||
const wordCount = csm.getWordCount();
|
||||
assert.ok(wordCount >= 30, `Fallback word list should have at least 30 words, got ${wordCount}`);
|
||||
|
||||
// Verify key categories are covered
|
||||
const politicsCheck = csm.checkLocalText('颠覆国家');
|
||||
assert.strictEqual(politicsCheck.hasViolation, true);
|
||||
|
||||
const pornCheck = csm.checkLocalText('色情');
|
||||
assert.strictEqual(pornCheck.hasViolation, true);
|
||||
|
||||
const gamblingCheck = csm.checkLocalText('赌博');
|
||||
assert.strictEqual(gamblingCheck.hasViolation, true);
|
||||
|
||||
const abuseCheck = csm.checkLocalText('傻逼');
|
||||
assert.strictEqual(abuseCheck.hasViolation, true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* wechatTokenManager.test.js
|
||||
* Unit tests for WeChat Access Token management.
|
||||
*/
|
||||
|
||||
const { describe, it, before, after } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
// Mock wx environment for WeChat mini game server
|
||||
// The wechatTokenManager uses wx APIs on client, but on server it uses
|
||||
// HTTPS requests to WeChat API. We test the server-side module.
|
||||
|
||||
const WechatTokenManager = require('../services/wechatTokenManager');
|
||||
|
||||
describe('WechatTokenManager', () => {
|
||||
let manager;
|
||||
|
||||
before(() => {
|
||||
manager = new WechatTokenManager();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
// Clean up any timers
|
||||
if (manager._refreshTimer) {
|
||||
clearInterval(manager._refreshTimer);
|
||||
}
|
||||
});
|
||||
|
||||
it('should initialize with no token', () => {
|
||||
assert.strictEqual(manager._accessToken, '');
|
||||
assert.strictEqual(manager._expiresAt, 0);
|
||||
assert.strictEqual(manager._isAvailable, false);
|
||||
});
|
||||
|
||||
it('should report unavailable when no token is set', () => {
|
||||
const available = manager.isAvailable();
|
||||
assert.strictEqual(available, false);
|
||||
});
|
||||
|
||||
it('should report available when valid token is set', () => {
|
||||
manager._accessToken = 'test_token_123';
|
||||
manager._expiresAt = Date.now() + 7000 * 1000; // 7000 seconds from now
|
||||
manager._isAvailable = true;
|
||||
assert.strictEqual(manager.isAvailable(), true);
|
||||
});
|
||||
|
||||
it('should return token when available', () => {
|
||||
manager._accessToken = 'test_token_456';
|
||||
manager._expiresAt = Date.now() + 7000 * 1000;
|
||||
manager._isAvailable = true;
|
||||
const token = manager.getAccessToken();
|
||||
assert.strictEqual(token, 'test_token_456');
|
||||
});
|
||||
|
||||
it('should return empty string when token is expired', () => {
|
||||
manager._accessToken = 'expired_token';
|
||||
manager._expiresAt = Date.now() - 1000; // expired
|
||||
manager._isAvailable = false;
|
||||
const token = manager.getAccessToken();
|
||||
assert.strictEqual(token, '');
|
||||
});
|
||||
|
||||
it('should handle token refresh failure gracefully', async () => {
|
||||
// Create a manager with invalid appid/secret to test failure
|
||||
const failManager = new WechatTokenManager();
|
||||
failManager.appId = 'invalid_appid';
|
||||
failManager.appSecret = 'invalid_secret';
|
||||
// Override retry delays to speed up test
|
||||
failManager._isRefreshing = false;
|
||||
|
||||
// The _refreshToken will attempt with retries, but should not throw.
|
||||
// Use a short timeout to avoid long waits.
|
||||
const result = await Promise.race([
|
||||
failManager._refreshToken(),
|
||||
new Promise((resolve) => setTimeout(() => resolve(false), 5000)),
|
||||
]);
|
||||
|
||||
assert.strictEqual(failManager.isAvailable(), false);
|
||||
// Clean up any pending timers
|
||||
failManager.destroy();
|
||||
});
|
||||
|
||||
it('should not leak token in toString', () => {
|
||||
manager._accessToken = 'sensitive_token_xyz';
|
||||
const str = manager.toString();
|
||||
assert.strictEqual(str.includes('sensitive_token_xyz'), false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user