Initial commit: Telegram Management System
Some checks failed
Deploy / deploy (push) Has been cancelled
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:
45
backend/test/.eslintrc.js
Normal file
45
backend/test/.eslintrc.js
Normal 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
233
backend/test/README.md
Normal 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
|
||||
638
backend/test/integration/TaskWorkflow.test.js
Normal file
638
backend/test/integration/TaskWorkflow.test.js
Normal 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
7
backend/test/mocha.opts
Normal 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
53
backend/test/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
532
backend/test/routers/SystemConfigRouter.test.js
Normal file
532
backend/test/routers/SystemConfigRouter.test.js
Normal 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('不支持的导出格式');
|
||||
});
|
||||
});
|
||||
});
|
||||
253
backend/test/services/AccountScheduler.test.js
Normal file
253
backend/test/services/AccountScheduler.test.js
Normal 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;
|
||||
});
|
||||
});
|
||||
});
|
||||
339
backend/test/services/RiskStrategyService.test.js
Normal file
339
backend/test/services/RiskStrategyService.test.js
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
472
backend/test/services/TaskExecutionEngine.test.js
Normal file
472
backend/test/services/TaskExecutionEngine.test.js
Normal 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
238
backend/test/setup.js
Normal 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;
|
||||
175
backend/test/testAccountHealthService.js
Normal file
175
backend/test/testAccountHealthService.js
Normal 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();
|
||||
107
backend/test/testAccountPoolAPI.js
Normal file
107
backend/test/testAccountPoolAPI.js
Normal 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();
|
||||
157
backend/test/testAccountPoolModels.js
Normal file
157
backend/test/testAccountPoolModels.js
Normal 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();
|
||||
Reference in New Issue
Block a user