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:
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user