Initial commit: Telegram Management System
Some checks failed
Deploy / deploy (push) Has been cancelled

Full-stack web application for Telegram management
- Frontend: Vue 3 + Vben Admin
- Backend: NestJS
- Features: User management, group broadcast, statistics

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
你的用户名
2025-11-04 15:37:50 +08:00
commit 237c7802e5
3674 changed files with 525172 additions and 0 deletions

45
backend/test/.eslintrc.js Normal file
View File

@@ -0,0 +1,45 @@
module.exports = {
env: {
node: true,
es2021: true,
mocha: true
},
extends: [
'eslint:recommended'
],
parserOptions: {
ecmaVersion: 12,
sourceType: 'module'
},
rules: {
'indent': ['error', 4],
'linebreak-style': ['error', 'unix'],
'quotes': ['error', 'single'],
'semi': ['error', 'always'],
'no-unused-vars': ['warn', { 'argsIgnorePattern': '^_' }],
'no-console': 'off', // Allow console in tests
'max-len': ['warn', { 'code': 120 }],
'prefer-const': 'error',
'no-var': 'error',
'object-shorthand': 'error',
'prefer-arrow-callback': 'error',
'prefer-template': 'error',
'template-curly-spacing': 'error',
'arrow-spacing': 'error',
'comma-dangle': ['error', 'never'],
'space-before-function-paren': ['error', 'never'],
'keyword-spacing': 'error',
'space-infix-ops': 'error',
'eol-last': 'error',
'no-trailing-spaces': 'error'
},
globals: {
'describe': 'readonly',
'it': 'readonly',
'before': 'readonly',
'after': 'readonly',
'beforeEach': 'readonly',
'afterEach': 'readonly',
'expect': 'readonly'
}
};

233
backend/test/README.md Normal file
View File

@@ -0,0 +1,233 @@
# Telegram Management System - Test Suite
This directory contains comprehensive unit and integration tests for the Telegram Management System backend.
## Test Structure
```
test/
├── setup.js # Test environment setup and utilities
├── services/ # Unit tests for service classes
│ ├── AccountScheduler.test.js
│ ├── RiskStrategyService.test.js
│ └── TaskExecutionEngine.test.js
├── routers/ # Unit tests for API routers
│ └── SystemConfigRouter.test.js
├── integration/ # Integration tests
│ └── TaskWorkflow.test.js
├── package.json # Test dependencies and scripts
├── mocha.opts # Mocha configuration
├── .eslintrc.js # ESLint configuration for tests
└── README.md # This file
```
## Test Categories
### Unit Tests
- **Service Tests**: Test individual service classes in isolation
- **Router Tests**: Test API endpoints and HTTP request/response handling
- **Model Tests**: Test database models and their relationships
### Integration Tests
- **Workflow Tests**: Test complete business workflows end-to-end
- **System Integration**: Test interaction between multiple services
- **External API Integration**: Test integration with external services
## Running Tests
### Prerequisites
```bash
cd backend/test
npm install
```
### Test Commands
```bash
# Run all tests
npm test
# Run unit tests only
npm run test:unit
# Run integration tests only
npm run test:integration
# Run router tests only
npm run test:routers
# Run tests with coverage report
npm run test:coverage
# Run tests in watch mode (for development)
npm run test:watch
# Lint test code
npm run lint
# Fix lint issues automatically
npm run lint:fix
```
## Test Environment
### Database
- Uses SQLite in-memory database for fast, isolated testing
- Test data is created and cleaned up automatically
- Database schema is synchronized before each test run
### Redis
- Uses `ioredis-mock` for Redis simulation
- No external Redis instance required for testing
- All Redis operations are mocked and isolated
### External Dependencies
- Telegram API calls are mocked using Sinon.js
- External HTTP requests are stubbed to avoid network dependencies
- File system operations use temporary directories
## Writing Tests
### Test File Structure
```javascript
const { expect } = require('chai');
const TestSetup = require('../setup');
const ServiceUnderTest = require('../../src/service/ServiceUnderTest');
describe('ServiceUnderTest', function() {
let service;
let testDb;
before(async function() {
this.timeout(10000);
await TestSetup.setupDatabase();
await TestSetup.setupRedis();
await TestSetup.createTestData();
testDb = TestSetup.getTestDb();
service = new ServiceUnderTest();
});
after(async function() {
await TestSetup.cleanup();
});
describe('Feature Group', function() {
it('should test specific behavior', async function() {
// Test implementation
});
});
});
```
### Best Practices
1. **Isolation**: Each test should be independent and not rely on other tests
2. **Cleanup**: Always clean up resources after tests complete
3. **Mocking**: Mock external dependencies to ensure tests are fast and reliable
4. **Assertions**: Use descriptive assertions with clear error messages
5. **Async/Await**: Use async/await for asynchronous operations
6. **Timeouts**: Set appropriate timeouts for long-running tests
### Test Data
The `setup.js` file provides methods for creating consistent test data:
```javascript
await TestSetup.createTestData(); // Creates accounts, tasks, and rules
const testAccount = await TestSetup.createTestAccount(customData);
const testTask = await TestSetup.createTestTask(customData);
```
## Coverage Requirements
The test suite aims for:
- **Lines**: 80% minimum coverage
- **Functions**: 80% minimum coverage
- **Branches**: 70% minimum coverage
- **Statements**: 80% minimum coverage
Coverage reports are generated in the `coverage/` directory when running `npm run test:coverage`.
## Continuous Integration
Tests are designed to run in CI/CD environments:
- No external dependencies required
- Fast execution (< 2 minutes for full suite)
- Deterministic results
- Proper error reporting
## Debugging Tests
### Running Individual Tests
```bash
# Run specific test file
./node_modules/.bin/mocha services/AccountScheduler.test.js
# Run specific test case
./node_modules/.bin/mocha -g "should select optimal account"
```
### Debug Mode
```bash
# Run with Node.js debugger
node --inspect-brk ./node_modules/.bin/mocha services/AccountScheduler.test.js
```
### Verbose Output
```bash
# Run with detailed output
npm test -- --reporter json > test-results.json
```
## Test Utilities
The test suite includes several utility functions:
- `TestSetup.setupDatabase()`: Initialize test database
- `TestSetup.setupRedis()`: Initialize Redis mock
- `TestSetup.createTestData()`: Create sample data
- `TestSetup.cleanup()`: Clean up all test resources
- `TestSetup.getTestDb()`: Get database instance
- `TestSetup.getTestRedis()`: Get Redis instance
## Common Issues
### Database Sync Errors
- Ensure `TestSetup.setupDatabase()` is called before tests
- Check that models are properly imported
### Redis Connection Issues
- Verify `TestSetup.setupRedis()` is called
- Ensure Redis operations are properly mocked
### Timeout Issues
- Increase timeout for slow tests using `this.timeout(10000)`
- Consider optimizing test setup and teardown
### Memory Leaks
- Always call `TestSetup.cleanup()` in `after()` hooks
- Close all database connections and clear intervals
## Contributing
When adding new tests:
1. Follow the existing test structure and naming conventions
2. Add appropriate test coverage for new features
3. Update this README if adding new test categories
4. Ensure all tests pass before submitting
5. Run linting and fix any style issues
## Performance
The test suite is optimized for speed:
- In-memory database for fast I/O
- Mocked external services
- Parallel test execution where possible
- Efficient setup and teardown procedures
Target execution times:
- Unit tests: < 30 seconds
- Integration tests: < 60 seconds
- Full suite: < 2 minutes

View File

@@ -0,0 +1,638 @@
const { expect } = require('chai');
const sinon = require('sinon');
const TestSetup = require('../setup');
// Import all services for integration testing
const TaskExecutionEngine = require('../../src/service/TaskExecutionEngine');
const AccountScheduler = require('../../src/service/AccountScheduler');
const RiskStrategyService = require('../../src/service/RiskStrategyService');
const BehaviorSimulationService = require('../../src/service/BehaviorSimulationService');
const ContentVariationService = require('../../src/service/ContentVariationService');
const AlertNotificationService = require('../../src/service/AlertNotificationService');
const MessageQueueService = require('../../src/service/MessageQueueService');
describe('Task Workflow Integration Tests', function() {
let testDb;
let testRedis;
let taskEngine;
let accountScheduler;
let riskService;
let behaviorService;
let contentService;
let alertService;
let queueService;
before(async function() {
this.timeout(20000);
// Setup test environment
await TestSetup.setupDatabase();
await TestSetup.setupRedis();
await TestSetup.createTestData();
testDb = TestSetup.getTestDb();
testRedis = TestSetup.getTestRedis();
// Initialize all services
taskEngine = new TaskExecutionEngine();
accountScheduler = new AccountScheduler();
riskService = new RiskStrategyService();
behaviorService = new BehaviorSimulationService();
contentService = new ContentVariationService();
alertService = new AlertNotificationService();
queueService = new MessageQueueService();
// Initialize services
await taskEngine.initialize();
await accountScheduler.start();
await riskService.initialize();
await behaviorService.initialize();
await contentService.initialize();
await alertService.initialize();
await queueService.initialize();
});
after(async function() {
// Cleanup all services
if (taskEngine) await taskEngine.shutdown();
if (accountScheduler) await accountScheduler.stop();
if (riskService) await riskService.shutdown();
if (behaviorService) await behaviorService.shutdown();
if (contentService) await contentService.shutdown();
if (alertService) await alertService.shutdown();
if (queueService) await queueService.shutdown();
await TestSetup.cleanup();
});
describe('Complete Task Execution Workflow', function() {
it('should execute a full task with all integrations', async function() {
this.timeout(15000);
const mockTask = {
id: 1,
name: 'Integration Test Task',
targetInfo: JSON.stringify({
targets: [
{ id: 'group1', name: 'Test Group 1', type: 'group' },
{ id: 'group2', name: 'Test Group 2', type: 'group' }
]
}),
messageContent: JSON.stringify({
content: 'Hello! This is a test message for integration testing.',
type: 'text'
}),
sendingStrategy: JSON.stringify({
type: 'sequential',
interval: 2000,
batchSize: 1,
enableRiskControl: true,
enableBehaviorSimulation: true,
enableContentVariation: true
}),
status: 'pending'
};
// Mock external Telegram API calls
const telegramSendStub = sinon.stub(taskEngine, 'sendTelegramMessage').resolves({
success: true,
messageId: `msg_${Date.now()}`,
timestamp: new Date(),
executionTime: 1200
});
// Execute the complete workflow
const result = await taskEngine.executeTask(mockTask);
// Verify overall execution success
expect(result).to.have.property('success', true);
expect(result).to.have.property('taskId', mockTask.id);
expect(result).to.have.property('totalTargets', 2);
expect(result).to.have.property('successCount');
expect(result).to.have.property('failureCount');
expect(result).to.have.property('executionTime');
// Verify all targets were processed
expect(result.successCount + result.failureCount).to.equal(2);
// Verify Telegram API was called for each target
expect(telegramSendStub.callCount).to.equal(2);
telegramSendStub.restore();
});
it('should handle risk-based task modification', async function() {
this.timeout(10000);
const riskTask = {
id: 2,
name: 'Risk Control Test Task',
targetInfo: JSON.stringify({
targets: [{ id: 'group1', name: 'Test Group', type: 'group' }]
}),
messageContent: JSON.stringify({
content: 'This message should trigger risk controls.',
type: 'text'
}),
sendingStrategy: JSON.stringify({
type: 'immediate',
enableRiskControl: true
}),
status: 'pending'
};
// Mock medium risk scenario
const riskEvalStub = sinon.stub(riskService, 'evaluateOverallRisk').resolves('medium');
const riskActionStub = sinon.stub(riskService, 'executeRiskAction').resolves({
action: 'delayed',
delay: 5000,
reason: 'Frequency threshold reached',
success: true
});
const telegramSendStub = sinon.stub(taskEngine, 'sendTelegramMessage').resolves({
success: true,
messageId: 'msg_risk_test',
timestamp: new Date()
});
const startTime = Date.now();
const result = await taskEngine.executeTask(riskTask);
const endTime = Date.now();
// Verify risk control was applied (should have been delayed)
expect(result.success).to.be.true;
expect(endTime - startTime).to.be.at.least(5000); // Should have been delayed
// Verify risk evaluation was called
expect(riskEvalStub.called).to.be.true;
expect(riskActionStub.called).to.be.true;
riskEvalStub.restore();
riskActionStub.restore();
telegramSendStub.restore();
});
it('should integrate account switching on health issues', async function() {
this.timeout(10000);
const accountTask = {
id: 3,
name: 'Account Health Test Task',
targetInfo: JSON.stringify({
targets: [{ id: 'group1', name: 'Test Group', type: 'group' }]
}),
messageContent: JSON.stringify({
content: 'Testing account health-based switching.',
type: 'text'
}),
sendingStrategy: JSON.stringify({
type: 'immediate',
enableRiskControl: true
}),
status: 'pending'
};
// Mock unhealthy account selection and switching
const selectStub = sinon.stub(accountScheduler, 'selectOptimalAccount');
selectStub.onFirstCall().resolves({
accountId: 3,
healthScore: 30, // Low health
status: 'warning',
tier: 'normal'
});
selectStub.onSecondCall().resolves({
accountId: 1,
healthScore: 85, // High health
status: 'active',
tier: 'normal'
});
const riskEvalStub = sinon.stub(riskService, 'evaluateOverallRisk').resolves('high');
const riskActionStub = sinon.stub(riskService, 'executeRiskAction').resolves({
action: 'switched',
originalAccount: 3,
newAccount: { accountId: 1, healthScore: 85 },
reason: 'Account health too low',
success: true
});
const telegramSendStub = sinon.stub(taskEngine, 'sendTelegramMessage').resolves({
success: true,
messageId: 'msg_health_test',
timestamp: new Date()
});
const result = await taskEngine.executeTask(accountTask);
// Verify task succeeded with account switching
expect(result.success).to.be.true;
expect(riskActionStub.called).to.be.true;
selectStub.restore();
riskEvalStub.restore();
riskActionStub.restore();
telegramSendStub.restore();
});
});
describe('Message Queue Integration', function() {
it('should process tasks through message queue', async function() {
this.timeout(10000);
const queuedTask = {
id: 'queue_integration_test',
taskData: {
id: 4,
name: 'Queued Task Test',
targetInfo: JSON.stringify({
targets: [{ id: 'group1', name: 'Test Group', type: 'group' }]
}),
messageContent: JSON.stringify({
content: 'This message was processed through the queue.',
type: 'text'
}),
sendingStrategy: JSON.stringify({
type: 'queued',
priority: 'normal'
})
},
priority: 'normal',
attempts: 0
};
// Mock queue processing methods on task engine
const processQueuedStub = sinon.stub(taskEngine, 'processQueuedTask').resolves({
success: true,
jobId: queuedTask.id,
processedAt: new Date(),
executionTime: 1500
});
// Add task to queue
const jobId = await queueService.addJob('task_execution', queuedTask, {
priority: queuedTask.priority
});
expect(jobId).to.not.be.null;
// Simulate queue processing
const result = await taskEngine.processQueuedTask(queuedTask);
expect(result).to.have.property('success', true);
expect(result).to.have.property('jobId', queuedTask.id);
processQueuedStub.restore();
});
it('should handle queue failures with retry mechanism', async function() {
this.timeout(10000);
const failingTask = {
id: 'failing_queue_test',
taskData: {
id: 5,
targetInfo: JSON.stringify({
targets: [{ id: 'group1', type: 'group' }]
}),
messageContent: JSON.stringify({
content: 'This task will fail initially.',
type: 'text'
})
},
attempts: 0,
maxRetries: 2
};
let callCount = 0;
const processStub = sinon.stub(taskEngine, 'processQueuedTaskWithRetry').callsFake(async () => {
callCount++;
if (callCount === 1) {
throw new Error('First attempt failed');
}
return {
success: true,
jobId: failingTask.id,
attempts: callCount,
processedAt: new Date()
};
});
const result = await taskEngine.processQueuedTaskWithRetry(failingTask);
expect(result.success).to.be.true;
expect(callCount).to.equal(2); // Failed once, succeeded on retry
processStub.restore();
});
});
describe('Real-time Monitoring Integration', function() {
it('should emit monitoring events during task execution', async function() {
this.timeout(10000);
const monitoringTask = {
id: 6,
name: 'Monitoring Integration Test',
targetInfo: JSON.stringify({
targets: [{ id: 'group1', name: 'Test Group', type: 'group' }]
}),
messageContent: JSON.stringify({
content: 'This task tests monitoring integration.',
type: 'text'
}),
sendingStrategy: JSON.stringify({
type: 'sequential',
enableMonitoring: true
}),
status: 'pending'
};
// Track monitoring events
const monitoringEvents = [];
taskEngine.on('taskStarted', (data) => {
monitoringEvents.push({ event: 'taskStarted', data });
});
taskEngine.on('taskProgress', (data) => {
monitoringEvents.push({ event: 'taskProgress', data });
});
taskEngine.on('taskCompleted', (data) => {
monitoringEvents.push({ event: 'taskCompleted', data });
});
const telegramSendStub = sinon.stub(taskEngine, 'sendTelegramMessage').resolves({
success: true,
messageId: 'msg_monitoring_test',
timestamp: new Date()
});
const result = await taskEngine.executeTask(monitoringTask);
// Verify task execution
expect(result.success).to.be.true;
// Verify monitoring events were emitted
expect(monitoringEvents.length).to.be.at.least(2); // At least start and complete
const startEvent = monitoringEvents.find(e => e.event === 'taskStarted');
const completeEvent = monitoringEvents.find(e => e.event === 'taskCompleted');
expect(startEvent).to.not.be.undefined;
expect(completeEvent).to.not.be.undefined;
telegramSendStub.restore();
});
it('should send alerts on critical issues', async function() {
this.timeout(10000);
const alertTask = {
id: 7,
name: 'Alert Integration Test',
targetInfo: JSON.stringify({
targets: [{ id: 'group1', name: 'Test Group', type: 'group' }]
}),
messageContent: JSON.stringify({
content: 'This task should trigger alerts.',
type: 'text'
}),
sendingStrategy: JSON.stringify({
type: 'immediate'
}),
status: 'pending'
};
// Mock critical risk that should trigger alerts
const riskEvalStub = sinon.stub(riskService, 'evaluateOverallRisk').resolves('critical');
const riskActionStub = sinon.stub(riskService, 'executeRiskAction').resolves({
action: 'blocked',
reason: 'Critical security risk detected',
success: false
});
// Mock alert sending
const alertStub = sinon.stub(alertService, 'sendAlert').resolves({
sent: true,
channels: ['websocket'],
timestamp: new Date()
});
const result = await taskEngine.executeTask(alertTask);
// Verify task was blocked
expect(result.success).to.be.false;
// Verify alert was sent
expect(alertStub.called).to.be.true;
riskEvalStub.restore();
riskActionStub.restore();
alertStub.restore();
});
});
describe('Content and Behavior Integration', function() {
it('should apply content variation and behavior simulation', async function() {
this.timeout(10000);
const contentTask = {
id: 8,
name: 'Content Behavior Integration Test',
targetInfo: JSON.stringify({
targets: [
{ id: 'group1', name: 'Test Group 1', type: 'group' },
{ id: 'group2', name: 'Test Group 2', type: 'group' }
]
}),
messageContent: JSON.stringify({
content: 'Hello world! This is a test message that should be varied.',
type: 'text'
}),
sendingStrategy: JSON.stringify({
type: 'sequential',
interval: 1000,
enableContentVariation: true,
enableBehaviorSimulation: true
}),
status: 'pending'
};
// Mock content variation
const variationStub = sinon.stub(contentService, 'generateVariation');
variationStub.onFirstCall().resolves({
content: 'Hi world! This is a test message that should be varied.',
variationsApplied: ['greeting_variation']
});
variationStub.onSecondCall().resolves({
content: 'Hello there! This is a test message that should be varied.',
variationsApplied: ['greeting_variation', 'casual_tone']
});
// Mock behavior simulation
const behaviorStub = sinon.stub(behaviorService, 'simulateHumanBehavior').resolves({
typingTime: 1500,
readingTime: 800,
delay: 300,
patterns: ['natural_typing', 'reading_pause']
});
const telegramSendStub = sinon.stub(taskEngine, 'sendTelegramMessage').resolves({
success: true,
messageId: 'msg_content_test',
timestamp: new Date()
});
const result = await taskEngine.executeTask(contentTask);
// Verify task execution
expect(result.success).to.be.true;
expect(result.totalTargets).to.equal(2);
// Verify content variation was applied
expect(variationStub.callCount).to.equal(2);
// Verify behavior simulation was applied
expect(behaviorStub.callCount).to.equal(2);
variationStub.restore();
behaviorStub.restore();
telegramSendStub.restore();
});
});
describe('Error Propagation and Recovery', function() {
it('should handle cascading service failures gracefully', async function() {
this.timeout(10000);
const errorTask = {
id: 9,
name: 'Error Handling Test',
targetInfo: JSON.stringify({
targets: [{ id: 'group1', name: 'Test Group', type: 'group' }]
}),
messageContent: JSON.stringify({
content: 'This task tests error handling.',
type: 'text'
}),
sendingStrategy: JSON.stringify({
type: 'immediate'
}),
status: 'pending'
};
// Mock service failures
const schedulerErrorStub = sinon.stub(accountScheduler, 'selectOptimalAccount')
.rejects(new Error('Account scheduler database connection failed'));
const result = await taskEngine.executeTask(errorTask);
// Verify graceful error handling
expect(result.success).to.be.false;
expect(result).to.have.property('error');
expect(result.error).to.include('account');
schedulerErrorStub.restore();
});
it('should recover from temporary service failures', async function() {
this.timeout(10000);
const recoveryTask = {
id: 10,
name: 'Recovery Test',
targetInfo: JSON.stringify({
targets: [{ id: 'group1', name: 'Test Group', type: 'group' }]
}),
messageContent: JSON.stringify({
content: 'This task tests recovery mechanisms.',
type: 'text'
}),
sendingStrategy: JSON.stringify({
type: 'immediate',
retryOnFailure: true,
maxRetries: 2
}),
status: 'pending'
};
// Mock temporary failure followed by success
let callCount = 0;
const telegramSendStub = sinon.stub(taskEngine, 'sendTelegramMessage').callsFake(() => {
callCount++;
if (callCount === 1) {
return Promise.reject(new Error('Temporary network failure'));
}
return Promise.resolve({
success: true,
messageId: 'msg_recovery_test',
timestamp: new Date()
});
});
const result = await taskEngine.executeTask(recoveryTask);
// Verify recovery was successful
expect(result.success).to.be.true;
expect(callCount).to.equal(2); // Failed once, succeeded on retry
telegramSendStub.restore();
});
});
describe('Performance Under Load', function() {
it('should handle multiple concurrent tasks', async function() {
this.timeout(20000);
const concurrentTasks = [];
// Create 5 concurrent tasks
for (let i = 0; i < 5; i++) {
concurrentTasks.push({
id: 100 + i,
name: `Concurrent Task ${i + 1}`,
targetInfo: JSON.stringify({
targets: [{ id: `group${i + 1}`, name: `Test Group ${i + 1}`, type: 'group' }]
}),
messageContent: JSON.stringify({
content: `Concurrent test message ${i + 1}`,
type: 'text'
}),
sendingStrategy: JSON.stringify({
type: 'immediate'
}),
status: 'pending'
});
}
// Mock Telegram API
const telegramSendStub = sinon.stub(taskEngine, 'sendTelegramMessage').resolves({
success: true,
messageId: 'concurrent_msg',
timestamp: new Date()
});
const startTime = Date.now();
// Execute all tasks concurrently
const promises = concurrentTasks.map(task => taskEngine.executeTask(task));
const results = await Promise.all(promises);
const endTime = Date.now();
const totalTime = endTime - startTime;
// Verify all tasks completed successfully
results.forEach((result, index) => {
expect(result.success).to.be.true;
expect(result.taskId).to.equal(100 + index);
});
// Verify reasonable performance (should complete within 10 seconds)
expect(totalTime).to.be.at.most(10000);
telegramSendStub.restore();
});
});
});

7
backend/test/mocha.opts Normal file
View File

@@ -0,0 +1,7 @@
--require ./setup.js
--timeout 30000
--recursive
--exit
--reporter spec
--slow 2000
--bail false

53
backend/test/package.json Normal file
View File

@@ -0,0 +1,53 @@
{
"name": "telegram-management-system-tests",
"version": "1.0.0",
"description": "Test suite for Telegram Management System",
"scripts": {
"test": "mocha --recursive --timeout 30000 --exit",
"test:unit": "mocha services/*.test.js --timeout 15000 --exit",
"test:integration": "mocha integration/*.test.js --timeout 30000 --exit",
"test:routers": "mocha routers/*.test.js --timeout 15000 --exit",
"test:coverage": "nyc mocha --recursive --timeout 30000 --exit",
"test:watch": "mocha --recursive --timeout 30000 --watch",
"lint": "eslint . --ext .js",
"lint:fix": "eslint . --ext .js --fix"
},
"devDependencies": {
"mocha": "^10.2.0",
"chai": "^4.3.10",
"sinon": "^17.0.1",
"nyc": "^15.1.0",
"eslint": "^8.55.0",
"supertest": "^6.3.3",
"ioredis-mock": "^8.9.0"
},
"dependencies": {
"sqlite3": "^5.1.6",
"sequelize": "^6.35.1",
"@hapi/hapi": "^21.3.2",
"moment": "^2.29.4"
},
"nyc": {
"exclude": [
"test/**",
"coverage/**",
"node_modules/**"
],
"reporter": [
"text",
"html",
"lcov"
],
"check-coverage": true,
"lines": 80,
"functions": 80,
"branches": 70,
"statements": 80
},
"mocha": {
"recursive": true,
"timeout": 30000,
"exit": true,
"reporter": "spec"
}
}

View File

@@ -0,0 +1,532 @@
const { expect } = require('chai');
const Hapi = require('@hapi/hapi');
const TestSetup = require('../setup');
const SystemConfigRouter = require('../../src/routers/SystemConfigRouter');
describe('SystemConfigRouter', function() {
let server;
let testDb;
before(async function() {
this.timeout(10000);
await TestSetup.setupDatabase();
await TestSetup.setupRedis();
testDb = TestSetup.getTestDb();
// Create Hapi server for testing
server = Hapi.server({
port: 0, // Use random port for testing
host: 'localhost'
});
// Register routes
const configRouter = new SystemConfigRouter(server);
const routes = configRouter.routes();
server.route(routes);
await server.start();
});
after(async function() {
if (server) {
await server.stop();
}
await TestSetup.cleanup();
});
describe('Configuration Retrieval', function() {
it('should get all configurations', async function() {
const response = await server.inject({
method: 'GET',
url: '/config'
});
expect(response.statusCode).to.equal(200);
const result = JSON.parse(response.payload);
expect(result).to.have.property('success', true);
expect(result.data).to.have.property('configs');
expect(result.data).to.have.property('total');
expect(result.data).to.have.property('timestamp');
});
it('should get specific configuration module', async function() {
const response = await server.inject({
method: 'GET',
url: '/config/system'
});
expect(response.statusCode).to.equal(200);
const result = JSON.parse(response.payload);
expect(result).to.have.property('success', true);
expect(result.data).to.have.property('configName', 'system');
expect(result.data).to.have.property('config');
expect(result.data.config).to.have.property('name');
expect(result.data.config).to.have.property('version');
});
it('should get specific configuration value', async function() {
const response = await server.inject({
method: 'GET',
url: '/config/system/debug'
});
expect(response.statusCode).to.equal(200);
const result = JSON.parse(response.payload);
expect(result).to.have.property('success', true);
expect(result.data).to.have.property('path', 'system.debug');
expect(result.data).to.have.property('value');
});
it('should return error for non-existent configuration', async function() {
const response = await server.inject({
method: 'GET',
url: '/config/nonexistent'
});
expect(response.statusCode).to.equal(200);
const result = JSON.parse(response.payload);
expect(result).to.have.property('success', false);
expect(result).to.have.property('message', '配置模块不存在');
});
});
describe('Configuration Updates', function() {
it('should update configuration module', async function() {
const updateData = {
config: {
name: 'Test System',
version: '1.1.0',
debug: true,
maintenance: false
},
persistent: false
};
const response = await server.inject({
method: 'PUT',
url: '/config/system',
payload: updateData
});
expect(response.statusCode).to.equal(200);
const result = JSON.parse(response.payload);
expect(result).to.have.property('success', true);
expect(result.data).to.have.property('configName', 'system');
expect(result.data).to.have.property('updated', true);
expect(result.data).to.have.property('persistent', false);
});
it('should set specific configuration value', async function() {
const updateData = {
value: true,
persistent: false
};
const response = await server.inject({
method: 'PUT',
url: '/config/system/maintenance',
payload: updateData
});
expect(response.statusCode).to.equal(200);
const result = JSON.parse(response.payload);
expect(result).to.have.property('success', true);
expect(result.data).to.have.property('path', 'system.maintenance');
expect(result.data).to.have.property('value', true);
});
it('should batch update configuration', async function() {
const updateData = {
updates: {
debug: false,
maintenance: true
},
persistent: false
};
const response = await server.inject({
method: 'POST',
url: '/config/system/batch',
payload: updateData
});
expect(response.statusCode).to.equal(200);
const result = JSON.parse(response.payload);
expect(result).to.have.property('success', true);
expect(result.data).to.have.property('configName', 'system');
expect(result.data).to.have.property('updatedKeys');
expect(result.data.updatedKeys).to.include('debug');
expect(result.data.updatedKeys).to.include('maintenance');
});
it('should validate configuration before update', async function() {
const invalidData = {
config: {
name: '', // Invalid empty name
version: null // Invalid version
}
};
const response = await server.inject({
method: 'PUT',
url: '/config/system',
payload: invalidData
});
expect(response.statusCode).to.equal(200);
const result = JSON.parse(response.payload);
expect(result).to.have.property('success', false);
expect(result.message).to.include('配置验证失败');
});
});
describe('Configuration Management', function() {
it('should reset configuration to default', async function() {
const response = await server.inject({
method: 'POST',
url: '/config/system/reset'
});
expect(response.statusCode).to.equal(200);
const result = JSON.parse(response.payload);
expect(result).to.have.property('success', true);
expect(result.data).to.have.property('configName', 'system');
expect(result.data).to.have.property('reset', true);
});
it('should validate configuration', async function() {
const validationData = {
config: {
name: 'Valid System',
version: '1.0.0',
debug: false,
maintenance: false
}
};
const response = await server.inject({
method: 'POST',
url: '/config/system/validate',
payload: validationData
});
expect(response.statusCode).to.equal(200);
const result = JSON.parse(response.payload);
expect(result).to.have.property('success', true);
expect(result.data).to.have.property('configName', 'system');
expect(result.data).to.have.property('valid', true);
expect(result.data).to.have.property('errors');
expect(result.data.errors).to.be.an('array').that.is.empty;
});
it('should save configuration to file', async function() {
const response = await server.inject({
method: 'POST',
url: '/config/system/save'
});
expect(response.statusCode).to.equal(200);
const result = JSON.parse(response.payload);
expect(result).to.have.property('success', true);
expect(result.data).to.have.property('configName', 'system');
expect(result.data).to.have.property('saved', true);
});
it('should reload all configurations', async function() {
const response = await server.inject({
method: 'POST',
url: '/config/reload'
});
expect(response.statusCode).to.equal(200);
const result = JSON.parse(response.payload);
expect(result).to.have.property('success', true);
expect(result.data).to.have.property('reloaded', true);
expect(result.data).to.have.property('configCount');
expect(result.data).to.have.property('configNames');
});
});
describe('Import/Export Operations', function() {
it('should export configurations', async function() {
const response = await server.inject({
method: 'GET',
url: '/config/export?format=json'
});
expect(response.statusCode).to.equal(200);
const result = JSON.parse(response.payload);
expect(result).to.have.property('success', true);
expect(result.data).to.have.property('timestamp');
expect(result.data).to.have.property('version');
expect(result.data).to.have.property('configs');
});
it('should export specific configurations', async function() {
const response = await server.inject({
method: 'GET',
url: '/config/export?configNames=system,database&format=json'
});
expect(response.statusCode).to.equal(200);
const result = JSON.parse(response.payload);
expect(result).to.have.property('success', true);
expect(result.data.configs).to.have.property('system');
expect(result.data.configs).to.have.property('database');
});
it('should import configurations', async function() {
const importData = {
importData: {
timestamp: new Date().toISOString(),
version: '1.0.0',
configs: {
system: {
name: 'Imported System',
version: '1.2.0',
debug: false,
maintenance: false
}
}
},
persistent: false
};
const response = await server.inject({
method: 'POST',
url: '/config/import',
payload: importData
});
expect(response.statusCode).to.equal(200);
const result = JSON.parse(response.payload);
expect(result).to.have.property('success', true);
expect(result.data).to.have.property('imported', true);
expect(result.data).to.have.property('results');
expect(result.data).to.have.property('summary');
expect(result.data.summary.success).to.be.at.least(1);
});
it('should handle invalid import data', async function() {
const invalidImportData = {
importData: {
// Missing configs property
timestamp: new Date().toISOString(),
version: '1.0.0'
}
};
const response = await server.inject({
method: 'POST',
url: '/config/import',
payload: invalidImportData
});
expect(response.statusCode).to.equal(200);
const result = JSON.parse(response.payload);
expect(result).to.have.property('success', false);
expect(result.message).to.include('导入配置失败');
});
});
describe('Service Management', function() {
it('should get service status', async function() {
const response = await server.inject({
method: 'GET',
url: '/config/service/status'
});
expect(response.statusCode).to.equal(200);
const result = JSON.parse(response.payload);
expect(result).to.have.property('success', true);
expect(result.data).to.have.property('isInitialized');
expect(result.data).to.have.property('configCount');
expect(result.data).to.have.property('watchersCount');
expect(result.data).to.have.property('configNames');
});
it('should restart service', async function() {
const response = await server.inject({
method: 'POST',
url: '/config/service/restart'
});
expect(response.statusCode).to.equal(200);
const result = JSON.parse(response.payload);
expect(result).to.have.property('success', true);
expect(result.data).to.have.property('restarted', true);
});
});
describe('Error Handling', function() {
it('should handle missing configuration data', async function() {
const response = await server.inject({
method: 'PUT',
url: '/config/system',
payload: {} // Missing config property
});
expect(response.statusCode).to.equal(200);
const result = JSON.parse(response.payload);
expect(result).to.have.property('success', false);
expect(result.message).to.include('无效的配置数据');
});
it('should handle missing batch update data', async function() {
const response = await server.inject({
method: 'POST',
url: '/config/system/batch',
payload: {} // Missing updates property
});
expect(response.statusCode).to.equal(200);
const result = JSON.parse(response.payload);
expect(result).to.have.property('success', false);
expect(result.message).to.include('无效的更新数据');
});
it('should handle missing validation data', async function() {
const response = await server.inject({
method: 'POST',
url: '/config/system/validate',
payload: {} // Missing config property
});
expect(response.statusCode).to.equal(200);
const result = JSON.parse(response.payload);
expect(result).to.have.property('success', false);
expect(result.message).to.include('缺少配置数据');
});
});
describe('Configuration Types Validation', function() {
it('should validate database configuration', async function() {
const databaseConfig = {
config: {
pool: {
max: 25,
min: 3,
acquire: 30000,
idle: 10000
},
retry: {
max: 3,
delay: 1000
}
}
};
const response = await server.inject({
method: 'PUT',
url: '/config/database',
payload: databaseConfig
});
expect(response.statusCode).to.equal(200);
const result = JSON.parse(response.payload);
expect(result).to.have.property('success', true);
});
it('should validate queue configuration', async function() {
const queueConfig = {
config: {
concurrency: 8,
retry: {
attempts: 5,
delay: 3000
},
timeout: 600000
}
};
const response = await server.inject({
method: 'PUT',
url: '/config/queue',
payload: queueConfig
});
expect(response.statusCode).to.equal(200);
const result = JSON.parse(response.payload);
expect(result).to.have.property('success', true);
});
it('should reject invalid database configuration', async function() {
const invalidConfig = {
config: {
pool: {
max: -5, // Invalid negative value
min: 10 // Min greater than max
}
}
};
const response = await server.inject({
method: 'PUT',
url: '/config/database',
payload: invalidConfig
});
expect(response.statusCode).to.equal(200);
const result = JSON.parse(response.payload);
expect(result).to.have.property('success', false);
expect(result.message).to.include('配置验证失败');
});
});
describe('File Operations', function() {
it('should handle export as file download', async function() {
const response = await server.inject({
method: 'GET',
url: '/config/export?format=file'
});
expect(response.statusCode).to.equal(200);
expect(response.headers['content-type']).to.include('application/json');
expect(response.headers['content-disposition']).to.include('attachment');
// Verify it's valid JSON content
const content = JSON.parse(response.payload);
expect(content).to.have.property('timestamp');
expect(content).to.have.property('configs');
});
it('should handle unsupported export format', async function() {
const response = await server.inject({
method: 'GET',
url: '/config/export?format=xml'
});
expect(response.statusCode).to.equal(200);
const result = JSON.parse(response.payload);
expect(result).to.have.property('success', false);
expect(result.message).to.include('不支持的导出格式');
});
});
});

View File

@@ -0,0 +1,253 @@
const { expect } = require('chai');
const TestSetup = require('../setup');
const AccountScheduler = require('../../src/service/AccountScheduler');
describe('AccountScheduler Service', function() {
let accountScheduler;
let testDb;
before(async function() {
this.timeout(10000);
// Setup test database and data
await TestSetup.setupDatabase();
await TestSetup.setupRedis();
await TestSetup.createTestData();
testDb = TestSetup.getTestDb();
accountScheduler = new AccountScheduler();
});
after(async function() {
await TestSetup.cleanup();
});
describe('Initialization', function() {
it('should initialize with default configuration', function() {
expect(accountScheduler).to.be.instanceOf(AccountScheduler);
expect(accountScheduler.schedulingStrategy).to.equal('health_priority');
expect(accountScheduler.isRunning).to.be.false;
});
it('should start and stop service correctly', async function() {
await accountScheduler.start();
expect(accountScheduler.isRunning).to.be.true;
await accountScheduler.stop();
expect(accountScheduler.isRunning).to.be.false;
});
});
describe('Account Selection', function() {
beforeEach(async function() {
await accountScheduler.start();
});
afterEach(async function() {
await accountScheduler.stop();
});
it('should select optimal account based on health priority', async function() {
const taskRequirements = {
tier: 'normal',
messageCount: 5,
urgency: 'medium'
};
const account = await accountScheduler.selectOptimalAccount(taskRequirements);
expect(account).to.not.be.null;
expect(account).to.have.property('accountId');
expect(account).to.have.property('healthScore');
expect(account.status).to.equal('active');
});
it('should exclude limited and banned accounts', async function() {
const taskRequirements = {
excludeStatuses: ['limited', 'banned'],
messageCount: 3
};
const account = await accountScheduler.selectOptimalAccount(taskRequirements);
if (account) {
expect(['active', 'warning']).to.include(account.status);
}
});
it('should respect account limits', async function() {
const taskRequirements = {
messageCount: 100, // Very high count
checkLimits: true
};
const account = await accountScheduler.selectOptimalAccount(taskRequirements);
if (account) {
expect(account.todaySentCount + taskRequirements.messageCount).to.be.at.most(account.dailyLimit);
}
});
});
describe('Load Balancing', function() {
beforeEach(async function() {
await accountScheduler.start();
});
afterEach(async function() {
await accountScheduler.stop();
});
it('should distribute tasks across multiple accounts', async function() {
const selections = [];
const taskRequirements = { messageCount: 1 };
// Select accounts multiple times
for (let i = 0; i < 5; i++) {
const account = await accountScheduler.selectOptimalAccount(taskRequirements);
if (account) {
selections.push(account.accountId);
}
}
// Should have some variety in account selection
const uniqueAccounts = new Set(selections);
expect(uniqueAccounts.size).to.be.greaterThan(0);
});
it('should update account usage after task completion', async function() {
const account = await accountScheduler.selectOptimalAccount({ messageCount: 5 });
if (account) {
const initialUsage = account.todaySentCount;
await accountScheduler.updateAccountUsage(account.accountId, {
sentCount: 5,
success: true,
executionTime: 1500
});
// Verify usage was updated
const updatedAccount = await accountScheduler.getAccountById(account.accountId);
expect(updatedAccount.todaySentCount).to.equal(initialUsage + 5);
}
});
});
describe('Risk Assessment', function() {
beforeEach(async function() {
await accountScheduler.start();
});
afterEach(async function() {
await accountScheduler.stop();
});
it('should calculate account risk score', async function() {
const account = await accountScheduler.selectOptimalAccount({ messageCount: 1 });
if (account) {
const riskScore = accountScheduler.calculateAccountRisk(account);
expect(riskScore).to.be.a('number');
expect(riskScore).to.be.at.least(0);
expect(riskScore).to.be.at.most(100);
}
});
it('should prefer lower risk accounts', async function() {
// Set strategy to risk-based
accountScheduler.setSchedulingStrategy('risk_balanced');
const account = await accountScheduler.selectOptimalAccount({
messageCount: 1,
riskTolerance: 'low'
});
if (account) {
expect(account.riskScore).to.be.at.most(50); // Low to medium risk
}
});
});
describe('Error Handling', function() {
it('should handle database connection errors gracefully', async function() {
// Mock database error
const originalQuery = testDb.query;
testDb.query = () => Promise.reject(new Error('Database connection lost'));
const account = await accountScheduler.selectOptimalAccount({ messageCount: 1 });
expect(account).to.be.null;
// Restore original query method
testDb.query = originalQuery;
});
it('should handle empty account pool', async function() {
// Clear all accounts
await testDb.query('DELETE FROM accounts_pool');
const account = await accountScheduler.selectOptimalAccount({ messageCount: 1 });
expect(account).to.be.null;
// Restore test data
await TestSetup.createTestData();
});
});
describe('Strategy Configuration', function() {
beforeEach(async function() {
await accountScheduler.start();
});
afterEach(async function() {
await accountScheduler.stop();
});
it('should support different scheduling strategies', function() {
const strategies = ['round_robin', 'health_priority', 'risk_balanced', 'random'];
strategies.forEach(strategy => {
accountScheduler.setSchedulingStrategy(strategy);
expect(accountScheduler.schedulingStrategy).to.equal(strategy);
});
});
it('should validate strategy parameters', function() {
expect(() => {
accountScheduler.setSchedulingStrategy('invalid_strategy');
}).to.throw();
});
});
describe('Performance Monitoring', function() {
beforeEach(async function() {
await accountScheduler.start();
});
afterEach(async function() {
await accountScheduler.stop();
});
it('should track selection performance metrics', async function() {
const startTime = Date.now();
await accountScheduler.selectOptimalAccount({ messageCount: 1 });
const endTime = Date.now();
const duration = endTime - startTime;
// Selection should be reasonably fast (under 100ms)
expect(duration).to.be.at.most(100);
});
it('should provide service statistics', function() {
const stats = accountScheduler.getServiceStats();
expect(stats).to.have.property('isRunning');
expect(stats).to.have.property('strategy');
expect(stats).to.have.property('totalSelections');
expect(stats.isRunning).to.be.true;
});
});
});

View File

@@ -0,0 +1,339 @@
const { expect } = require('chai');
const TestSetup = require('../setup');
const RiskStrategyService = require('../../src/service/RiskStrategyService');
describe('RiskStrategyService', function() {
let riskService;
let testDb;
before(async function() {
this.timeout(10000);
await TestSetup.setupDatabase();
await TestSetup.setupRedis();
await TestSetup.createTestData();
testDb = TestSetup.getTestDb();
riskService = new RiskStrategyService();
await riskService.initialize();
});
after(async function() {
await TestSetup.cleanup();
});
describe('Initialization', function() {
it('should initialize with default rules', function() {
expect(riskService.isInitialized).to.be.true;
expect(riskService.activeRules.size).to.be.greaterThan(0);
});
it('should load rules from database', async function() {
const rules = await riskService.getAllRules();
expect(rules).to.be.an('array');
expect(rules.length).to.be.greaterThan(0);
});
});
describe('Risk Evaluation', function() {
const mockExecutionContext = {
account: { accountId: 1, healthScore: 85, status: 'active' },
target: { id: 'test_group', type: 'group' },
message: { content: 'Test message', length: 12 },
task: { id: 1, strategy: 'sequential' },
timing: { hour: 14, dayOfWeek: 3 }
};
it('should evaluate overall risk level', async function() {
const riskLevel = await riskService.evaluateOverallRisk(mockExecutionContext);
expect(riskLevel).to.be.oneOf(['low', 'medium', 'high', 'critical']);
});
it('should identify specific risks', async function() {
const risks = await riskService.identifyRisks(mockExecutionContext);
expect(risks).to.be.an('array');
risks.forEach(risk => {
expect(risk).to.have.property('ruleId');
expect(risk).to.have.property('severity');
expect(risk).to.have.property('action');
expect(risk).to.have.property('reason');
});
});
it('should handle account health risk', async function() {
const lowHealthContext = {
...mockExecutionContext,
account: { accountId: 2, healthScore: 30, status: 'warning' }
};
const risks = await riskService.identifyRisks(lowHealthContext);
const healthRisk = risks.find(r => r.type === 'account' && r.category === 'health');
if (healthRisk) {
expect(healthRisk.severity).to.be.oneOf(['medium', 'high']);
expect(healthRisk.action).to.be.oneOf(['switched', 'delayed', 'blocked']);
}
});
it('should handle frequency limits', async function() {
// Simulate high frequency context
const highFreqContext = {
...mockExecutionContext,
frequency: { recentCount: 15, timeWindow: 3600 }
};
const risks = await riskService.identifyRisks(highFreqContext);
const freqRisk = risks.find(r => r.category === 'frequency');
if (freqRisk) {
expect(freqRisk.action).to.be.oneOf(['delayed', 'blocked']);
}
});
});
describe('Risk Action Execution', function() {
it('should execute delayed action', async function() {
const risk = {
ruleId: 1,
action: 'delayed',
severity: 'medium',
parameters: { minDelay: 5000, maxDelay: 15000 }
};
const result = await riskService.executeRiskAction(risk, {});
expect(result).to.have.property('action', 'delayed');
expect(result).to.have.property('delay');
expect(result.delay).to.be.at.least(5000);
expect(result.delay).to.be.at.most(15000);
});
it('should execute switch account action', async function() {
const risk = {
ruleId: 2,
action: 'switched',
severity: 'high',
parameters: { reason: 'account_health_low' }
};
const result = await riskService.executeRiskAction(risk, {
account: { accountId: 1 }
});
expect(result).to.have.property('action', 'switched');
expect(result).to.have.property('originalAccount', 1);
expect(result).to.have.property('reason');
});
it('should execute block action', async function() {
const risk = {
ruleId: 3,
action: 'blocked',
severity: 'critical',
parameters: { reason: 'critical_risk_detected' }
};
const result = await riskService.executeRiskAction(risk, {});
expect(result).to.have.property('action', 'blocked');
expect(result).to.have.property('reason');
expect(result.success).to.be.false;
});
});
describe('Rule Management', function() {
it('should create new rule', async function() {
const ruleData = {
name: 'Test Rule',
type: 'behavior',
category: 'test',
conditions: { testCondition: true },
action: 'warned',
severity: 'low',
priority: 50,
enabled: true
};
const rule = await riskService.createRule(ruleData);
expect(rule).to.have.property('id');
expect(rule.name).to.equal('Test Rule');
expect(rule.enabled).to.be.true;
});
it('should update existing rule', async function() {
const rules = await riskService.getAllRules();
const ruleToUpdate = rules[0];
const updateData = {
priority: 90,
enabled: false
};
const updatedRule = await riskService.updateRule(ruleToUpdate.id, updateData);
expect(updatedRule.priority).to.equal(90);
expect(updatedRule.enabled).to.be.false;
});
it('should delete rule', async function() {
const rules = await riskService.getAllRules();
const ruleToDelete = rules.find(r => r.name === 'Test Rule');
if (ruleToDelete) {
const result = await riskService.deleteRule(ruleToDelete.id);
expect(result).to.be.true;
const updatedRules = await riskService.getAllRules();
const deletedRule = updatedRules.find(r => r.id === ruleToDelete.id);
expect(deletedRule).to.be.undefined;
}
});
});
describe('Risk Statistics', function() {
it('should provide risk statistics', async function() {
const stats = await riskService.getRiskStatistics('24h');
expect(stats).to.have.property('totalEvaluations');
expect(stats).to.have.property('riskDistribution');
expect(stats).to.have.property('actionDistribution');
expect(stats).to.have.property('topTriggeredRules');
expect(stats.riskDistribution).to.have.property('low');
expect(stats.riskDistribution).to.have.property('medium');
expect(stats.riskDistribution).to.have.property('high');
expect(stats.riskDistribution).to.have.property('critical');
});
it('should track rule performance', async function() {
const performance = await riskService.getRulePerformance();
expect(performance).to.be.an('array');
performance.forEach(rule => {
expect(rule).to.have.property('ruleId');
expect(rule).to.have.property('triggerCount');
expect(rule).to.have.property('avgProcessingTime');
expect(rule).to.have.property('successRate');
});
});
});
describe('Configuration Management', function() {
it('should update risk thresholds', async function() {
const newThresholds = {
low: { min: 0, max: 30 },
medium: { min: 31, max: 60 },
high: { min: 61, max: 85 },
critical: { min: 86, max: 100 }
};
await riskService.updateRiskThresholds(newThresholds);
const config = riskService.getConfiguration();
expect(config.riskThresholds).to.deep.equal(newThresholds);
});
it('should enable/disable rule categories', async function() {
await riskService.enableRuleCategory('behavior', false);
const config = riskService.getConfiguration();
expect(config.enabledCategories.behavior).to.be.false;
// Re-enable for other tests
await riskService.enableRuleCategory('behavior', true);
});
});
describe('Integration Tests', function() {
it('should work with message queue for async processing', async function() {
const mockContext = {
account: { accountId: 1, healthScore: 85 },
target: { id: 'test_group' },
message: { content: 'Test message' }
};
// This would normally interact with Redis queue
const queueResult = await riskService.queueRiskEvaluation(mockContext);
expect(queueResult).to.have.property('queued', true);
expect(queueResult).to.have.property('jobId');
});
it('should provide real-time risk monitoring data', async function() {
const monitoringData = await riskService.getRealTimeRiskData();
expect(monitoringData).to.have.property('currentRiskLevel');
expect(monitoringData).to.have.property('activeThreats');
expect(monitoringData).to.have.property('recentEvaluations');
expect(monitoringData).to.have.property('systemHealth');
});
});
describe('Error Handling', function() {
it('should handle malformed rule conditions', async function() {
const invalidContext = {
account: null, // Invalid account
target: { id: 'test' },
message: { content: 'test' }
};
const risks = await riskService.identifyRisks(invalidContext);
expect(risks).to.be.an('array'); // Should not throw error
});
it('should handle database connection errors', async function() {
// Mock database error
const originalQuery = testDb.query;
testDb.query = () => Promise.reject(new Error('Database error'));
try {
await riskService.getAllRules();
expect.fail('Should have thrown an error');
} catch (error) {
expect(error.message).to.include('Database error');
}
// Restore
testDb.query = originalQuery;
});
});
describe('Performance Tests', function() {
it('should evaluate risks quickly', async function() {
const context = {
account: { accountId: 1, healthScore: 85 },
target: { id: 'test_group' },
message: { content: 'Test message' }
};
const startTime = Date.now();
await riskService.identifyRisks(context);
const endTime = Date.now();
const duration = endTime - startTime;
expect(duration).to.be.at.most(50); // Should complete within 50ms
});
it('should handle concurrent evaluations', async function() {
const promises = [];
const context = {
account: { accountId: 1, healthScore: 85 },
target: { id: 'test_group' },
message: { content: 'Test message' }
};
// Create 10 concurrent evaluations
for (let i = 0; i < 10; i++) {
promises.push(riskService.identifyRisks({ ...context, requestId: i }));
}
const results = await Promise.all(promises);
expect(results).to.have.length(10);
results.forEach(result => {
expect(result).to.be.an('array');
});
});
});
});

View File

@@ -0,0 +1,472 @@
const { expect } = require('chai');
const sinon = require('sinon');
const TestSetup = require('../setup');
const TaskExecutionEngine = require('../../src/service/TaskExecutionEngine');
describe('TaskExecutionEngine', function() {
let taskEngine;
let testDb;
before(async function() {
this.timeout(15000);
await TestSetup.setupDatabase();
await TestSetup.setupRedis();
await TestSetup.createTestData();
testDb = TestSetup.getTestDb();
taskEngine = new TaskExecutionEngine();
await taskEngine.initialize();
});
after(async function() {
if (taskEngine) {
await taskEngine.shutdown();
}
await TestSetup.cleanup();
});
describe('Initialization', function() {
it('should initialize all components', function() {
expect(taskEngine.isInitialized).to.be.true;
expect(taskEngine.accountScheduler).to.not.be.null;
expect(taskEngine.riskService).to.not.be.null;
expect(taskEngine.behaviorSimulator).to.not.be.null;
expect(taskEngine.contentVariator).to.not.be.null;
});
it('should start and stop correctly', async function() {
await taskEngine.start();
expect(taskEngine.isRunning).to.be.true;
await taskEngine.stop();
expect(taskEngine.isRunning).to.be.false;
// Restart for other tests
await taskEngine.start();
});
});
describe('Task Execution Workflow', function() {
let mockTask;
beforeEach(function() {
mockTask = {
id: 1,
name: 'Test Task',
targetInfo: JSON.stringify({
targets: [
{ id: 'group1', name: 'Test Group 1', type: 'group' },
{ id: 'group2', name: 'Test Group 2', type: 'group' }
]
}),
messageContent: JSON.stringify({
content: 'Hello, this is a test message!',
type: 'text'
}),
sendingStrategy: JSON.stringify({
type: 'sequential',
interval: 2000,
batchSize: 1
}),
status: 'pending'
};
});
it('should execute task successfully', async function() {
this.timeout(10000);
// Mock the actual Telegram sending
const sendStub = sinon.stub(taskEngine, 'sendTelegramMessage').resolves({
success: true,
messageId: 'msg_123',
timestamp: new Date()
});
const result = await taskEngine.executeTask(mockTask);
expect(result).to.have.property('success', true);
expect(result).to.have.property('taskId', mockTask.id);
expect(result).to.have.property('totalTargets', 2);
expect(result).to.have.property('successCount');
expect(result).to.have.property('failureCount');
sendStub.restore();
});
it('should handle task execution with risk controls', async function() {
this.timeout(10000);
// Mock risk evaluation that returns medium risk
const riskStub = sinon.stub(taskEngine.riskService, 'evaluateOverallRisk').resolves('medium');
const actionStub = sinon.stub(taskEngine.riskService, 'executeRiskAction').resolves({
action: 'delayed',
delay: 3000,
success: true
});
const sendStub = sinon.stub(taskEngine, 'sendTelegramMessage').resolves({
success: true,
messageId: 'msg_123'
});
const result = await taskEngine.executeTask(mockTask);
expect(result.success).to.be.true;
expect(riskStub.called).to.be.true;
riskStub.restore();
actionStub.restore();
sendStub.restore();
});
it('should handle account switching on high risk', async function() {
this.timeout(10000);
// Mock high risk scenario requiring account switch
const riskStub = sinon.stub(taskEngine.riskService, 'evaluateOverallRisk').resolves('high');
const actionStub = sinon.stub(taskEngine.riskService, 'executeRiskAction').resolves({
action: 'switched',
originalAccount: 1,
newAccount: { accountId: 2, healthScore: 90 },
success: true
});
const sendStub = sinon.stub(taskEngine, 'sendTelegramMessage').resolves({
success: true,
messageId: 'msg_124'
});
const result = await taskEngine.executeTask(mockTask);
expect(result.success).to.be.true;
expect(actionStub.called).to.be.true;
riskStub.restore();
actionStub.restore();
sendStub.restore();
});
});
describe('Message Processing', function() {
it('should apply content variation', async function() {
const originalMessage = 'Hello world! This is a test message.';
const context = {
account: { accountId: 1 },
target: { id: 'group1' },
variationLevel: 'medium'
};
// Mock content variation
const variationStub = sinon.stub(taskEngine.contentVariator, 'generateVariation').resolves({
content: 'Hi world! This is a test message.',
variationsApplied: ['greeting_variation']
});
const result = await taskEngine.processMessageContent(originalMessage, context);
expect(result).to.have.property('content');
expect(result).to.have.property('variationsApplied');
expect(variationStub.called).to.be.true;
variationStub.restore();
});
it('should apply behavior simulation', async function() {
const context = {
account: { accountId: 1, tier: 'normal' },
message: { content: 'Test message', length: 12 }
};
// Mock behavior simulation
const behaviorStub = sinon.stub(taskEngine.behaviorSimulator, 'simulateHumanBehavior').resolves({
typingTime: 1200,
readingTime: 800,
delay: 500,
patterns: ['natural_typing', 'reading_pause']
});
const result = await taskEngine.simulateBehavior(context);
expect(result).to.have.property('typingTime');
expect(result).to.have.property('readingTime');
expect(result).to.have.property('delay');
expect(behaviorStub.called).to.be.true;
behaviorStub.restore();
});
});
describe('Account Management Integration', function() {
it('should select optimal account for task', async function() {
const taskRequirements = {
tier: 'normal',
messageCount: 5,
urgency: 'medium'
};
// Mock account selection
const scheduleStub = sinon.stub(taskEngine.accountScheduler, 'selectOptimalAccount').resolves({
accountId: 1,
healthScore: 85,
status: 'active',
tier: 'normal'
});
const account = await taskEngine.selectAccount(taskRequirements);
expect(account).to.not.be.null;
expect(account).to.have.property('accountId');
expect(scheduleStub.called).to.be.true;
scheduleStub.restore();
});
it('should update account usage after successful send', async function() {
const accountId = 1;
const usageData = {
sentCount: 1,
success: true,
executionTime: 1500,
riskLevel: 'low'
};
// Mock usage update
const updateStub = sinon.stub(taskEngine.accountScheduler, 'updateAccountUsage').resolves(true);
await taskEngine.updateAccountUsage(accountId, usageData);
expect(updateStub.calledWith(accountId, usageData)).to.be.true;
updateStub.restore();
});
});
describe('Error Handling', function() {
it('should handle send failures gracefully', async function() {
const mockTask = {
id: 1,
targetInfo: JSON.stringify({
targets: [{ id: 'group1', type: 'group' }]
}),
messageContent: JSON.stringify({
content: 'Test message'
}),
sendingStrategy: JSON.stringify({
type: 'sequential',
interval: 1000
})
};
// Mock send failure
const sendStub = sinon.stub(taskEngine, 'sendTelegramMessage').rejects(
new Error('Rate limit exceeded')
);
const result = await taskEngine.executeTask(mockTask);
expect(result).to.have.property('success', false);
expect(result).to.have.property('failureCount');
expect(result.failureCount).to.be.greaterThan(0);
sendStub.restore();
});
it('should handle account unavailability', async function() {
// Mock no available accounts
const scheduleStub = sinon.stub(taskEngine.accountScheduler, 'selectOptimalAccount').resolves(null);
const mockTask = {
id: 1,
targetInfo: JSON.stringify({
targets: [{ id: 'group1', type: 'group' }]
}),
messageContent: JSON.stringify({
content: 'Test message'
}),
sendingStrategy: JSON.stringify({
type: 'sequential'
})
};
const result = await taskEngine.executeTask(mockTask);
expect(result).to.have.property('success', false);
expect(result).to.have.property('error');
expect(result.error).to.include('account');
scheduleStub.restore();
});
it('should handle critical risk blocking', async function() {
// Mock critical risk that blocks execution
const riskStub = sinon.stub(taskEngine.riskService, 'evaluateOverallRisk').resolves('critical');
const actionStub = sinon.stub(taskEngine.riskService, 'executeRiskAction').resolves({
action: 'blocked',
reason: 'Critical security risk detected',
success: false
});
const mockTask = {
id: 1,
targetInfo: JSON.stringify({
targets: [{ id: 'group1', type: 'group' }]
}),
messageContent: JSON.stringify({
content: 'Test message'
}),
sendingStrategy: JSON.stringify({
type: 'sequential'
})
};
const result = await taskEngine.executeTask(mockTask);
expect(result).to.have.property('success', false);
expect(result).to.have.property('error');
expect(result.error).to.include('blocked');
riskStub.restore();
actionStub.restore();
});
});
describe('Performance Monitoring', function() {
it('should track execution metrics', async function() {
const mockTask = {
id: 1,
targetInfo: JSON.stringify({
targets: [{ id: 'group1', type: 'group' }]
}),
messageContent: JSON.stringify({
content: 'Test message'
}),
sendingStrategy: JSON.stringify({
type: 'sequential'
})
};
// Mock successful send
const sendStub = sinon.stub(taskEngine, 'sendTelegramMessage').resolves({
success: true,
messageId: 'msg_123',
executionTime: 1200
});
const startTime = Date.now();
const result = await taskEngine.executeTask(mockTask);
const endTime = Date.now();
expect(result).to.have.property('executionTime');
expect(result.executionTime).to.be.at.most(endTime - startTime + 100); // Allow some margin
sendStub.restore();
});
it('should provide service statistics', function() {
const stats = taskEngine.getServiceStats();
expect(stats).to.have.property('isRunning');
expect(stats).to.have.property('totalTasksExecuted');
expect(stats).to.have.property('successRate');
expect(stats).to.have.property('avgExecutionTime');
expect(stats).to.have.property('activeConnections');
});
});
describe('Configuration Management', function() {
it('should update execution configuration', async function() {
const newConfig = {
maxConcurrentTasks: 10,
defaultTimeout: 30000,
retryAttempts: 3,
enableBehaviorSimulation: true
};
await taskEngine.updateConfiguration(newConfig);
const config = taskEngine.getConfiguration();
expect(config.maxConcurrentTasks).to.equal(10);
expect(config.defaultTimeout).to.equal(30000);
});
it('should validate configuration parameters', function() {
const invalidConfig = {
maxConcurrentTasks: -1, // Invalid
defaultTimeout: 'invalid' // Invalid type
};
expect(() => {
taskEngine.updateConfiguration(invalidConfig);
}).to.throw();
});
});
describe('Integration with Message Queue', function() {
it('should process queued tasks', async function() {
const queuedTask = {
id: 'queue_task_1',
taskData: {
id: 1,
targetInfo: JSON.stringify({
targets: [{ id: 'group1', type: 'group' }]
}),
messageContent: JSON.stringify({
content: 'Queued message'
}),
sendingStrategy: JSON.stringify({
type: 'sequential'
})
},
priority: 'normal'
};
// Mock queue processing
const sendStub = sinon.stub(taskEngine, 'sendTelegramMessage').resolves({
success: true,
messageId: 'msg_queue_123'
});
const result = await taskEngine.processQueuedTask(queuedTask);
expect(result).to.have.property('success', true);
expect(result).to.have.property('jobId', queuedTask.id);
sendStub.restore();
});
it('should handle queue failures with retry', async function() {
const queuedTask = {
id: 'retry_task_1',
taskData: {
id: 1,
targetInfo: JSON.stringify({
targets: [{ id: 'group1', type: 'group' }]
}),
messageContent: JSON.stringify({
content: 'Retry message'
})
},
attempts: 0,
maxRetries: 3
};
// Mock failure on first attempt, success on second
let callCount = 0;
const sendStub = sinon.stub(taskEngine, 'sendTelegramMessage').callsFake(() => {
callCount++;
if (callCount === 1) {
return Promise.reject(new Error('Temporary failure'));
}
return Promise.resolve({ success: true, messageId: 'msg_retry_123' });
});
const result = await taskEngine.processQueuedTaskWithRetry(queuedTask);
expect(result).to.have.property('success', true);
expect(callCount).to.equal(2); // Failed once, succeeded on retry
sendStub.restore();
});
});
});

238
backend/test/setup.js Normal file
View File

@@ -0,0 +1,238 @@
const { Sequelize } = require('sequelize');
const Redis = require('ioredis');
// Test database configuration
const testDb = new Sequelize({
dialect: 'sqlite',
storage: ':memory:',
logging: false, // Disable logging during tests
define: {
timestamps: true,
underscored: false
}
});
// Test Redis client (using redis-mock for testing)
const MockRedis = require('ioredis-mock');
const testRedis = new MockRedis();
// Mock services for testing
class TestSetup {
static async setupDatabase() {
try {
// Import all models
const MAccountPool = require('../src/modes/MAccountPool');
const MAccountHealthScore = require('../src/modes/MAccountHealthScore');
const MAccountUsageRecord = require('../src/modes/MAccountUsageRecord');
const MGroupTask = require('../src/modes/MGroupTask');
const MRiskRule = require('../src/modes/MRiskRule');
const MRiskLog = require('../src/modes/MRiskLog');
const MAnomalyLog = require('../src/modes/MAnomalyLog');
// Override database instance for testing
const models = [
MAccountPool, MAccountHealthScore, MAccountUsageRecord,
MGroupTask, MRiskRule, MRiskLog, MAnomalyLog
];
// Recreate models with test database
for (const model of models) {
if (model.sequelize) {
// Re-define model with test database
const modelName = model.name;
const attributes = model.rawAttributes;
const options = model.options;
testDb.define(modelName, attributes, {
...options,
tableName: options.tableName || modelName
});
}
}
// Sync database
await testDb.sync({ force: true });
console.log('Test database setup complete');
} catch (error) {
console.error('Test database setup failed:', error);
throw error;
}
}
static async setupRedis() {
// Clear all Redis data
await testRedis.flushall();
console.log('Test Redis setup complete');
}
static async createTestData() {
try {
// Create test accounts
const testAccounts = [
{
accountId: 1,
phone: '+1234567890',
status: 'active',
tier: 'normal',
healthScore: 85,
dailyLimit: 50,
hourlyLimit: 10,
totalSentCount: 100,
todaySentCount: 5,
consecutiveFailures: 0,
riskScore: 15,
priority: 60,
isActive: true
},
{
accountId: 2,
phone: '+1234567891',
status: 'warning',
tier: 'new',
healthScore: 65,
dailyLimit: 30,
hourlyLimit: 5,
totalSentCount: 20,
todaySentCount: 2,
consecutiveFailures: 1,
riskScore: 35,
priority: 40,
isActive: true
},
{
accountId: 3,
phone: '+1234567892',
status: 'limited',
tier: 'trusted',
healthScore: 45,
dailyLimit: 100,
hourlyLimit: 15,
totalSentCount: 500,
todaySentCount: 8,
consecutiveFailures: 3,
riskScore: 75,
priority: 20,
isActive: false
}
];
// Insert test accounts using raw SQL to avoid model issues
for (const account of testAccounts) {
await testDb.query(`
INSERT INTO accounts_pool
(accountId, phone, status, tier, healthScore, dailyLimit, hourlyLimit,
totalSentCount, todaySentCount, consecutiveFailures, riskScore, priority, isActive, createdAt, updatedAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
`, {
replacements: [
account.accountId, account.phone, account.status, account.tier,
account.healthScore, account.dailyLimit, account.hourlyLimit,
account.totalSentCount, account.todaySentCount, account.consecutiveFailures,
account.riskScore, account.priority, account.isActive
]
});
}
// Create test tasks
const testTasks = [
{
name: 'Test Task 1',
status: 'completed',
targetInfo: JSON.stringify({ targets: [{ id: 1, name: 'Group 1' }] }),
messageContent: JSON.stringify({ content: 'Test message 1' }),
sendingStrategy: JSON.stringify({ type: 'sequential', interval: 1000 }),
successCount: 10,
failureCount: 2,
processedCount: 12
},
{
name: 'Test Task 2',
status: 'running',
targetInfo: JSON.stringify({ targets: [{ id: 2, name: 'Group 2' }] }),
messageContent: JSON.stringify({ content: 'Test message 2' }),
sendingStrategy: JSON.stringify({ type: 'parallel', interval: 2000 }),
successCount: 5,
failureCount: 1,
processedCount: 6
}
];
for (const task of testTasks) {
await testDb.query(`
INSERT INTO group_tasks
(name, status, targetInfo, messageContent, sendingStrategy, successCount, failureCount, processedCount, createdAt, updatedAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
`, {
replacements: [
task.name, task.status, task.targetInfo, task.messageContent,
task.sendingStrategy, task.successCount, task.failureCount, task.processedCount
]
});
}
// Create test risk rules
const testRiskRules = [
{
name: 'Frequency Limit',
type: 'behavior',
category: 'frequency',
conditions: JSON.stringify({ timeWindow: 3600, threshold: 10 }),
action: 'delayed',
severity: 'medium',
priority: 70,
enabled: true
},
{
name: 'Account Health Check',
type: 'account',
category: 'health',
conditions: JSON.stringify({ healthThreshold: 50 }),
action: 'switched',
severity: 'high',
priority: 80,
enabled: true
}
];
for (const rule of testRiskRules) {
await testDb.query(`
INSERT INTO risk_rules
(name, type, category, conditions, action, severity, priority, enabled, triggerCount, createdAt, updatedAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, datetime('now'), datetime('now'))
`, {
replacements: [
rule.name, rule.type, rule.category, rule.conditions,
rule.action, rule.severity, rule.priority, rule.enabled
]
});
}
console.log('Test data created successfully');
} catch (error) {
console.error('Failed to create test data:', error);
throw error;
}
}
static async cleanup() {
try {
await testDb.close();
await testRedis.disconnect();
console.log('Test cleanup complete');
} catch (error) {
console.error('Test cleanup failed:', error);
}
}
static getTestDb() {
return testDb;
}
static getTestRedis() {
return testRedis;
}
}
module.exports = TestSetup;

View File

@@ -0,0 +1,175 @@
// 测试账号健康度评分系统
require('module-alias/register');
const Db = require("@src/config/Db");
const MAccountPool = require("@src/modes/MAccountPool");
const MAccountUsageLog = require("@src/modes/MAccountUsageLog");
const MTgAccount = require("@src/modes/MTgAccount");
const AccountHealthService = require("@src/service/AccountHealthService");
const initAssociations = require("@src/modes/initAssociations");
const moment = require("moment");
async function testHealthService() {
try {
console.log("开始测试账号健康度服务...\n");
// 初始化数据库
await Db.getInstance();
// 等待数据库连接完成
await new Promise(resolve => setTimeout(resolve, 2000));
// 初始化关联关系
initAssociations();
// 确保所有表都创建
const { Sequelize } = require('sequelize');
const db = Db.getInstance().db;
await db.sync({ alter: false });
// 获取服务实例
const healthService = AccountHealthService.getInstance();
// 获取或创建测试账号
let tgAccount = await MTgAccount.findOne();
if (!tgAccount) {
console.log("创建测试TG账号...");
tgAccount = await MTgAccount.create({
phone: '+1234567890',
status: 1,
name: 'Test Account'
});
}
// 创建或获取账号池记录
let accountPool = await MAccountPool.findOne({
where: { accountId: tgAccount.id }
});
if (!accountPool) {
console.log("创建账号池记录...");
accountPool = await MAccountPool.create({
accountId: tgAccount.id,
phone: tgAccount.phone,
status: 'active',
tier: 'normal',
dailyLimit: 50,
hourlyLimit: 10,
totalSentCount: 150,
todaySentCount: 20
});
}
console.log(`✅ 使用账号池ID: ${accountPool.id}, 手机号: ${accountPool.phone}\n`);
// 创建模拟使用记录
console.log("创建模拟使用记录...");
await createMockUsageLogs(accountPool.id);
// 1. 测试单个账号健康度评估
console.log("\n=== 测试单个账号健康度评估 ===");
const healthResult = await healthService.evaluateAccountHealth(accountPool.id);
console.log("健康度评估结果:");
console.log(`- 健康分数: ${healthResult.healthScore.toFixed(2)}`);
console.log(`- 健康状态: ${healthResult.status}`);
console.log(`- 健康趋势: ${healthResult.trend}`);
console.log("- 改善建议:");
healthResult.recommendations.forEach(rec => {
console.log(`${rec}`);
});
console.log("\n关键指标:");
console.log(`- 成功率: ${healthResult.metrics.successRate.toFixed(2)}%`);
console.log(`- 错误率: ${healthResult.metrics.errorRate.toFixed(2)}%`);
console.log(`- 日使用率: ${healthResult.metrics.dailyUsageRate.toFixed(2)}%`);
console.log(`- 平均响应时间: ${healthResult.metrics.avgResponseTime.toFixed(0)}ms`);
console.log(`- 互动率: ${healthResult.metrics.engagementRate.toFixed(2)}%`);
console.log(`- 异常分数: ${healthResult.metrics.anomalyScore.toFixed(2)}`);
// 2. 测试健康度报告
console.log("\n=== 测试健康度报告 ===");
const report = await healthService.getHealthReport(accountPool.id, 30);
if (report) {
console.log("健康度报告:");
console.log(`- 当前分数: ${report.currentScore.toFixed(2)}`);
console.log(`- 当前状态: ${report.currentStatus}`);
console.log(`- 平均分数: ${report.metrics.avgScore.toFixed(2)}`);
console.log(`- 最低分数: ${report.metrics.minScore.toFixed(2)}`);
console.log(`- 最高分数: ${report.metrics.maxScore.toFixed(2)}`);
}
// 3. 测试批量评估
console.log("\n=== 测试批量账号评估 ===");
const batchResult = await healthService.evaluateAllActiveAccounts();
console.log(`批量评估完成: 总计 ${batchResult.total} 个账号`);
console.log(`成功评估: ${batchResult.evaluated} 个账号`);
// 4. 检查账号状态更新
await accountPool.reload();
console.log("\n=== 账号状态更新 ===");
console.log(`- 账号状态: ${accountPool.status}`);
console.log(`- 账号分级: ${accountPool.tier}`);
console.log(`- 风险评分: ${accountPool.riskScore.toFixed(2)}`);
console.log("\n✅ 所有测试完成!");
} catch (error) {
console.error("❌ 测试失败:", error);
} finally {
process.exit();
}
}
// 创建模拟使用记录
async function createMockUsageLogs(accountPoolId) {
const logs = [];
const now = moment();
// 创建过去7天的使用记录
for (let day = 0; day < 7; day++) {
const date = moment().subtract(day, 'days');
// 每天创建10-20条记录
const recordCount = 10 + Math.floor(Math.random() * 10);
for (let i = 0; i < recordCount; i++) {
const hour = 8 + Math.floor(Math.random() * 12); // 8-20点之间
const startTime = date.hour(hour).minute(Math.floor(Math.random() * 60));
const duration = 1000 + Math.floor(Math.random() * 4000); // 1-5秒
// 80%成功率
const isSuccess = Math.random() < 0.8;
logs.push({
accountPoolId,
taskId: 1,
taskType: 'group_send',
groupId: null, // 不设置群组ID避免外键约束
messageContent: '测试消息内容',
status: isSuccess ? 'success' : (Math.random() < 0.5 ? 'failed' : 'timeout'),
errorCode: isSuccess ? null : 'ERR_SEND_FAILED',
errorMessage: isSuccess ? null : '发送失败',
startTime: startTime.toDate(),
endTime: startTime.add(duration, 'milliseconds').toDate(),
duration,
riskLevel: Math.random() < 0.7 ? 'low' : (Math.random() < 0.8 ? 'medium' : 'high'),
behaviorSimulation: {
typingSpeed: 60 + Math.floor(Math.random() * 40),
pauseTime: 500 + Math.floor(Math.random() * 1500)
},
recipientCount: 20 + Math.floor(Math.random() * 30),
readCount: Math.floor(15 + Math.random() * 20),
replyCount: Math.floor(Math.random() * 5),
reportCount: Math.random() < 0.05 ? 1 : 0 // 5%的举报率
});
}
}
// 批量创建记录
await MAccountUsageLog.bulkCreate(logs);
console.log(`✅ 创建了 ${logs.length} 条模拟使用记录`);
}
// 运行测试
testHealthService();

View File

@@ -0,0 +1,107 @@
// 测试账号池管理API
const axios = require('axios');
const API_BASE = 'http://localhost:3333';
const TOKEN = 'your-auth-token-here'; // 需要替换为实际的token
const api = axios.create({
baseURL: API_BASE,
headers: {
'token': TOKEN,
'Content-Type': 'application/json'
}
});
async function testAPIs() {
try {
console.log('开始测试账号池API...\n');
// 1. 测试获取账号池列表
console.log('=== 测试获取账号池列表 ===');
try {
const listResponse = await api.get('/accountpool/list', {
params: {
page: 1,
pageSize: 10,
sortBy: 'createdAt',
sortOrder: 'DESC'
}
});
console.log('✅ 获取列表成功:', {
total: listResponse.data.data.pagination.total,
count: listResponse.data.data.list.length
});
} catch (error) {
console.log('❌ 获取列表失败:', error.response?.data || error.message);
}
// 2. 测试获取统计数据
console.log('\n=== 测试获取统计数据 ===');
try {
const statsResponse = await api.get('/accountpool/statistics');
console.log('✅ 获取统计成功:', statsResponse.data.data.summary);
} catch (error) {
console.log('❌ 获取统计失败:', error.response?.data || error.message);
}
// 3. 测试批量评估健康度
console.log('\n=== 测试批量评估健康度 ===');
try {
const evaluateResponse = await api.post('/accountpool/evaluate/batch');
console.log('✅ 批量评估成功:', evaluateResponse.data.data.result);
} catch (error) {
console.log('❌ 批量评估失败:', error.response?.data || error.message);
}
// 4. 如果有账号,测试单个账号的详情
console.log('\n=== 测试获取账号详情 ===');
try {
const listResponse = await api.get('/accountpool/list?pageSize=1');
if (listResponse.data.data.list.length > 0) {
const accountId = listResponse.data.data.list[0].id;
const detailResponse = await api.get(`/accountpool/detail/${accountId}`);
console.log('✅ 获取详情成功:', {
id: detailResponse.data.data.account.id,
phone: detailResponse.data.data.account.phone,
status: detailResponse.data.data.account.status,
healthScore: detailResponse.data.data.account.healthRecords?.[0]?.healthScore
});
// 5. 测试健康度报告
console.log('\n=== 测试健康度报告 ===');
const reportResponse = await api.get(`/accountpool/health/report/${accountId}?days=7`);
if (reportResponse.data.success && reportResponse.data.data.report) {
console.log('✅ 获取报告成功:', {
currentScore: reportResponse.data.data.report.currentScore,
trend: reportResponse.data.data.report.trend
});
}
} else {
console.log('⚠️ 没有账号可供测试详情');
}
} catch (error) {
console.log('❌ 测试详情失败:', error.response?.data || error.message);
}
console.log('\n✅ API测试完成');
} catch (error) {
console.error('❌ 测试失败:', error.message);
}
}
// 获取token的函数
async function getAuthToken() {
try {
// 这里应该调用登录API获取token
// 临时使用硬编码的token进行测试
console.log('请先获取有效的token并更新脚本中的TOKEN变量');
return null;
} catch (error) {
console.error('获取token失败:', error);
return null;
}
}
// 运行测试
testAPIs();

View File

@@ -0,0 +1,157 @@
// 测试账号池相关模型
require('module-alias/register');
const Db = require("@src/config/Db");
const MAccountPool = require("@src/modes/MAccountPool");
const MAccountHealth = require("@src/modes/MAccountHealth");
const MAccountUsageLog = require("@src/modes/MAccountUsageLog");
const MTgAccount = require("@src/modes/MTgAccount");
const initAssociations = require("@src/modes/initAssociations");
async function testModels() {
try {
console.log("开始测试账号池模型...");
// 初始化数据库
await Db.getInstance();
// 初始化关联关系
initAssociations();
// 同步模型(创建表)- 使用 {alter: true} 来处理已存在的表
await MAccountPool.sync({ alter: false });
await MAccountHealth.sync({ alter: false });
await MAccountUsageLog.sync({ alter: false });
console.log("✅ 数据表创建成功");
// 清理已有的测试数据
await MAccountUsageLog.destroy({ where: {} });
await MAccountHealth.destroy({ where: {} });
await MAccountPool.destroy({ where: {} });
console.log("✅ 已清理旧数据");
// 获取第一个TG账号用于测试
const tgAccount = await MTgAccount.findOne();
if (!tgAccount) {
console.log("❌ 没有找到TG账号请先创建TG账号");
return;
}
// 1. 创建账号池记录
const accountPool = await MAccountPool.create({
accountId: tgAccount.id,
phone: tgAccount.phone,
status: 'active',
tier: 'new',
dailyLimit: 30,
hourlyLimit: 5,
intervalSeconds: 120,
tags: ['test', 'new_account'],
metadata: {
source: 'test_script',
createdBy: 'system'
}
});
console.log("✅ 账号池记录创建成功:", {
id: accountPool.id,
phone: accountPool.phone,
status: accountPool.status,
tier: accountPool.tier
});
// 2. 创建健康度记录
const healthRecord = await MAccountHealth.create({
accountPoolId: accountPool.id,
healthScore: 95,
successRate: 98.5,
errorCount: 2,
warningCount: 1,
activeHours: [9, 10, 11, 14, 15, 16, 17, 18],
evaluationDetails: {
lastCheck: new Date(),
metrics: {
responseTime: 250,
successRate: 98.5
}
},
recommendations: [
"建议减少发送频率",
"避免在凌晨发送消息"
]
});
console.log("✅ 健康度记录创建成功:", {
id: healthRecord.id,
healthScore: healthRecord.healthScore,
trend: healthRecord.trend
});
// 3. 创建使用记录
const usageLog = await MAccountUsageLog.create({
accountPoolId: accountPool.id,
taskId: 1, // 假设的任务ID
taskType: 'group_send',
groupId: 1,
messageContent: '测试消息内容',
status: 'success',
startTime: new Date(),
endTime: new Date(Date.now() + 5000),
duration: 5000,
riskLevel: 'low',
behaviorSimulation: {
typingSpeed: 80,
pauseTime: 1000,
readTime: 2000
},
recipientCount: 50,
readCount: 45,
replyCount: 5
});
console.log("✅ 使用记录创建成功:", {
id: usageLog.id,
status: usageLog.status,
duration: usageLog.duration
});
// 4. 测试关联查询
const poolWithRelations = await MAccountPool.findOne({
where: { id: accountPool.id },
include: [
{
model: MTgAccount,
as: 'tgAccount'
},
{
model: MAccountHealth,
as: 'healthRecords',
limit: 5,
order: [['createdAt', 'DESC']]
},
{
model: MAccountUsageLog,
as: 'usageLogs',
limit: 10,
order: [['createdAt', 'DESC']]
}
]
});
console.log("✅ 关联查询成功:", {
accountPhone: poolWithRelations.tgAccount?.phone,
healthRecordCount: poolWithRelations.healthRecords?.length || 0,
usageLogCount: poolWithRelations.usageLogs?.length || 0
});
console.log("\n✅ 所有测试通过!账号池模型工作正常。");
} catch (error) {
console.error("❌ 测试失败:", error);
} finally {
process.exit();
}
}
// 运行测试
testModels();