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

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

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

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

View File

@@ -0,0 +1,149 @@
import { jest } from '@jest/globals';
import { authenticate, generateToken, generateRefreshToken } from '../../../../../services/api-gateway/src/middleware/auth.js';
import jwt from 'jsonwebtoken';
// Mock dependencies
jest.mock('jsonwebtoken');
jest.mock('../../../../../services/api-gateway/src/utils/cache.js', () => ({
cache: {
get: jest.fn(),
set: jest.fn(),
del: jest.fn()
}
}));
describe('Auth Middleware', () => {
let req, res, next;
beforeEach(() => {
req = {
headers: {},
user: null
};
res = {
status: jest.fn(() => res),
json: jest.fn(() => res)
};
next = jest.fn();
jest.clearAllMocks();
});
describe('authenticate', () => {
it('should authenticate valid token', async () => {
const token = 'valid-token';
const decodedToken = {
userId: 'user123',
role: 'user',
permissions: ['read', 'write']
};
req.headers.authorization = `Bearer ${token}`;
jwt.verify.mockReturnValue(decodedToken);
await authenticate(req, res, next);
expect(jwt.verify).toHaveBeenCalledWith(token, process.env.JWT_SECRET);
expect(req.user).toEqual(decodedToken);
expect(req.token).toBe(token);
expect(next).toHaveBeenCalled();
});
it('should reject missing authorization header', async () => {
await authenticate(req, res, next);
expect(res.status).toHaveBeenCalledWith(401);
expect(res.json).toHaveBeenCalledWith({
success: false,
error: 'No token provided'
});
expect(next).not.toHaveBeenCalled();
});
it('should reject invalid token format', async () => {
req.headers.authorization = 'InvalidFormat token';
await authenticate(req, res, next);
expect(res.status).toHaveBeenCalledWith(401);
expect(res.json).toHaveBeenCalledWith({
success: false,
error: 'Invalid token format'
});
});
it('should reject expired token', async () => {
req.headers.authorization = 'Bearer expired-token';
jwt.verify.mockImplementation(() => {
throw new Error('jwt expired');
});
await authenticate(req, res, next);
expect(res.status).toHaveBeenCalledWith(401);
expect(res.json).toHaveBeenCalledWith({
success: false,
error: 'Token expired'
});
});
it('should reject blacklisted token', async () => {
const token = 'blacklisted-token';
req.headers.authorization = `Bearer ${token}`;
const { cache } = require('../../../../../services/api-gateway/src/utils/cache.js');
cache.get.mockResolvedValue('1');
await authenticate(req, res, next);
expect(cache.get).toHaveBeenCalledWith(`blacklist:${token}`);
expect(res.status).toHaveBeenCalledWith(401);
expect(res.json).toHaveBeenCalledWith({
success: false,
error: 'Token has been revoked'
});
});
});
describe('generateToken', () => {
it('should generate access token with correct payload', () => {
const payload = {
userId: 'user123',
role: 'admin',
permissions: ['all']
};
const expectedToken = 'generated-token';
jwt.sign.mockReturnValue(expectedToken);
const token = generateToken(payload);
expect(jwt.sign).toHaveBeenCalledWith(
payload,
process.env.JWT_SECRET,
{ expiresIn: '24h' }
);
expect(token).toBe(expectedToken);
});
});
describe('generateRefreshToken', () => {
it('should generate refresh token with correct payload', () => {
const payload = {
userId: 'user123',
role: 'user'
};
const expectedToken = 'refresh-token';
jwt.sign.mockReturnValue(expectedToken);
const token = generateRefreshToken(payload);
expect(jwt.sign).toHaveBeenCalledWith(
payload,
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
expect(token).toBe(expectedToken);
});
});
});

View File

@@ -0,0 +1,86 @@
import { jest } from '@jest/globals';
import { globalRateLimiter, strictRateLimiter, dynamicRateLimiter } from '../../../../../services/api-gateway/src/middleware/rateLimiter.js';
// Mock Redis store
jest.mock('rate-limit-redis', () => ({
default: jest.fn(() => ({
increment: jest.fn(),
decrement: jest.fn(),
resetKey: jest.fn()
}))
}));
describe('Rate Limiter Middleware', () => {
let req, res, next;
beforeEach(() => {
req = {
ip: '127.0.0.1',
user: { id: 'user123', role: 'user' },
path: '/api/v1/test'
};
res = {
status: jest.fn(() => res),
json: jest.fn(() => res),
set: jest.fn(() => res)
};
next = jest.fn();
jest.clearAllMocks();
});
describe('globalRateLimiter', () => {
it('should allow request within rate limit', async () => {
// Mock the rate limiter to allow the request
const middleware = globalRateLimiter;
// Since globalRateLimiter is created by express-rate-limit,
// we need to test its configuration
expect(middleware).toBeDefined();
expect(typeof middleware).toBe('function');
});
it('should set rate limit headers', async () => {
// Test that rate limit headers are set correctly
res.setHeader = jest.fn();
// Mock a successful request
const mockRateLimitInfo = {
limit: 100,
current: 25,
remaining: 75,
resetTime: new Date(Date.now() + 60000)
};
// Test headers would be set by the middleware
expect(res.setHeader).not.toHaveBeenCalled(); // Initially not called
});
});
describe('strictRateLimiter', () => {
it('should have stricter limits than global', () => {
expect(strictRateLimiter).toBeDefined();
expect(typeof strictRateLimiter).toBe('function');
});
});
describe('dynamicRateLimiter', () => {
it('should apply different limits based on user role', async () => {
// Test for admin user
req.user.role = 'admin';
expect(dynamicRateLimiter).toBeDefined();
// Test for regular user
req.user.role = 'user';
expect(dynamicRateLimiter).toBeDefined();
// Test for viewer
req.user.role = 'viewer';
expect(dynamicRateLimiter).toBeDefined();
});
it('should apply default limits for unauthenticated users', async () => {
req.user = null;
expect(dynamicRateLimiter).toBeDefined();
});
});
});

View File

@@ -0,0 +1,282 @@
import { jest } from '@jest/globals';
import CampaignService from '../../../../services/orchestrator/src/services/campaignService.js';
import Campaign from '../../../../services/orchestrator/src/models/Campaign.js';
import { createCampaign } from '../../../helpers/factories.js';
// Mock dependencies
jest.mock('../../../../services/orchestrator/src/models/Campaign.js');
jest.mock('../../../../services/orchestrator/src/services/messagingService.js');
jest.mock('../../../../services/orchestrator/src/services/analyticsService.js');
describe('CampaignService', () => {
let campaignService;
let mockMessagingService;
let mockAnalyticsService;
beforeEach(() => {
mockMessagingService = {
sendMessage: jest.fn(),
validateMessage: jest.fn()
};
mockAnalyticsService = {
trackEvent: jest.fn(),
updateCampaignStats: jest.fn()
};
campaignService = new CampaignService(mockMessagingService, mockAnalyticsService);
jest.clearAllMocks();
});
describe('createCampaign', () => {
it('should create a new campaign', async () => {
const campaignData = createCampaign();
const savedCampaign = { ...campaignData, _id: 'camp123', save: jest.fn() };
Campaign.mockImplementation(() => savedCampaign);
savedCampaign.save.mockResolvedValue(savedCampaign);
const result = await campaignService.createCampaign(campaignData, 'user123');
expect(Campaign).toHaveBeenCalledWith({
...campaignData,
createdBy: 'user123'
});
expect(savedCampaign.save).toHaveBeenCalled();
expect(result).toEqual(savedCampaign);
});
it('should validate required fields', async () => {
const invalidData = { name: '' };
await expect(campaignService.createCampaign(invalidData, 'user123'))
.rejects.toThrow('Campaign name is required');
});
it('should track campaign creation', async () => {
const campaignData = createCampaign();
const savedCampaign = { ...campaignData, _id: 'camp123', save: jest.fn() };
Campaign.mockImplementation(() => savedCampaign);
savedCampaign.save.mockResolvedValue(savedCampaign);
await campaignService.createCampaign(campaignData, 'user123');
expect(mockAnalyticsService.trackEvent).toHaveBeenCalledWith({
event: 'campaign.created',
campaignId: 'camp123',
userId: 'user123',
campaignType: campaignData.type
});
});
});
describe('executeCampaign', () => {
it('should execute campaign successfully', async () => {
const campaign = {
_id: 'camp123',
status: 'active',
targeting: {
includedUsers: ['user1', 'user2', 'user3']
},
content: {
customMessage: 'Test message'
},
settings: {
rateLimit: {
messagesPerSecond: 10
}
},
save: jest.fn()
};
Campaign.findById.mockResolvedValue(campaign);
mockMessagingService.sendMessage.mockResolvedValue({ success: true });
const result = await campaignService.executeCampaign('camp123', {
test: false
});
expect(result.campaignId).toBe('camp123');
expect(result.status).toBe('completed');
expect(result.progress.total).toBe(3);
expect(mockMessagingService.sendMessage).toHaveBeenCalledTimes(3);
});
it('should handle test mode execution', async () => {
const campaign = {
_id: 'camp123',
status: 'active',
targeting: {
includedUsers: ['user1', 'user2', 'user3']
},
content: {
customMessage: 'Test message'
},
save: jest.fn()
};
Campaign.findById.mockResolvedValue(campaign);
mockMessagingService.sendMessage.mockResolvedValue({ success: true });
const result = await campaignService.executeCampaign('camp123', {
test: true,
testUsers: ['testUser1']
});
expect(result.isTest).toBe(true);
expect(mockMessagingService.sendMessage).toHaveBeenCalledTimes(1);
expect(mockMessagingService.sendMessage).toHaveBeenCalledWith(
'testUser1',
expect.any(Object)
);
});
it('should respect rate limits', async () => {
const campaign = {
_id: 'camp123',
status: 'active',
targeting: {
includedUsers: Array(100).fill(null).map((_, i) => `user${i}`)
},
content: {
customMessage: 'Test message'
},
settings: {
rateLimit: {
messagesPerSecond: 10
}
},
save: jest.fn()
};
Campaign.findById.mockResolvedValue(campaign);
mockMessagingService.sendMessage.mockResolvedValue({ success: true });
const startTime = Date.now();
await campaignService.executeCampaign('camp123', { test: false });
const endTime = Date.now();
// With 100 users and 10 messages per second, it should take at least 9 seconds
// (allowing for some margin)
expect(endTime - startTime).toBeGreaterThanOrEqual(9000);
});
it('should handle campaign not found', async () => {
Campaign.findById.mockResolvedValue(null);
await expect(campaignService.executeCampaign('invalid123'))
.rejects.toThrow('Campaign not found');
});
it('should handle inactive campaign', async () => {
const campaign = {
_id: 'camp123',
status: 'draft'
};
Campaign.findById.mockResolvedValue(campaign);
await expect(campaignService.executeCampaign('camp123'))
.rejects.toThrow('Campaign is not active');
});
});
describe('updateCampaign', () => {
it('should update campaign successfully', async () => {
const campaign = {
_id: 'camp123',
name: 'Old Name',
description: 'Old Description',
save: jest.fn(),
toObject: jest.fn(() => ({ _id: 'camp123', name: 'New Name' }))
};
Campaign.findById.mockResolvedValue(campaign);
campaign.save.mockResolvedValue(campaign);
const updates = {
name: 'New Name',
description: 'New Description'
};
const result = await campaignService.updateCampaign('camp123', updates);
expect(campaign.name).toBe('New Name');
expect(campaign.description).toBe('New Description');
expect(campaign.save).toHaveBeenCalled();
});
it('should not allow updating campaign in progress', async () => {
const campaign = {
_id: 'camp123',
status: 'executing'
};
Campaign.findById.mockResolvedValue(campaign);
await expect(campaignService.updateCampaign('camp123', { name: 'New' }))
.rejects.toThrow('Cannot update campaign while it is executing');
});
});
describe('deleteCampaign', () => {
it('should delete campaign successfully', async () => {
const campaign = {
_id: 'camp123',
status: 'draft',
deleteOne: jest.fn()
};
Campaign.findById.mockResolvedValue(campaign);
await campaignService.deleteCampaign('camp123');
expect(campaign.deleteOne).toHaveBeenCalled();
});
it('should not allow deleting active campaign', async () => {
const campaign = {
_id: 'camp123',
status: 'active'
};
Campaign.findById.mockResolvedValue(campaign);
await expect(campaignService.deleteCampaign('camp123'))
.rejects.toThrow('Cannot delete active campaign');
});
});
describe('getCampaignStatistics', () => {
it('should return campaign statistics', async () => {
const campaign = {
_id: 'camp123',
statistics: {
messagesSent: 100,
delivered: 95,
read: 80,
clicked: 20,
conversions: 10
}
};
Campaign.findById.mockResolvedValue(campaign);
const stats = await campaignService.getCampaignStatistics('camp123');
expect(stats).toEqual({
overview: {
messagesSent: 100,
delivered: 95,
deliveryRate: 95,
read: 80,
readRate: 84.21,
clicked: 20,
clickRate: 25,
conversions: 10,
conversionRate: 12.5
}
});
});
});
});

View File

@@ -0,0 +1,377 @@
import { jest } from '@jest/globals';
import CampaignSchedulerService from '../../../../services/scheduler/src/services/campaignSchedulerService.js';
import ScheduledCampaign from '../../../../services/scheduler/src/models/ScheduledCampaign.js';
import ScheduleJob from '../../../../services/scheduler/src/models/ScheduleJob.js';
import { createScheduledCampaign } from '../../../helpers/factories.js';
// Mock dependencies
jest.mock('../../../../services/scheduler/src/models/ScheduledCampaign.js');
jest.mock('../../../../services/scheduler/src/models/ScheduleJob.js');
jest.mock('node-cron');
jest.mock('bull');
describe('CampaignSchedulerService', () => {
let schedulerService;
let mockQueue;
beforeEach(() => {
mockQueue = {
add: jest.fn(),
process: jest.fn(),
on: jest.fn()
};
schedulerService = new CampaignSchedulerService(mockQueue);
jest.clearAllMocks();
});
describe('createSchedule', () => {
it('should create one-time schedule', async () => {
const scheduleData = createScheduledCampaign('camp123', {
type: 'one-time',
schedule: {
startDateTime: new Date('2024-06-01T10:00:00Z')
}
});
const savedSchedule = {
...scheduleData,
_id: 'sched123',
save: jest.fn().mockResolvedValue(scheduleData)
};
ScheduledCampaign.mockImplementation(() => savedSchedule);
const result = await schedulerService.createSchedule(scheduleData);
expect(ScheduledCampaign).toHaveBeenCalledWith(scheduleData);
expect(savedSchedule.save).toHaveBeenCalled();
expect(mockQueue.add).toHaveBeenCalledWith(
expect.objectContaining({
scheduleId: 'sched123',
campaignId: 'camp123'
}),
expect.objectContaining({
delay: expect.any(Number)
})
);
});
it('should create recurring schedule', async () => {
const scheduleData = createScheduledCampaign('camp123', {
type: 'recurring',
schedule: {
startDateTime: new Date(),
recurring: {
pattern: 'daily',
frequency: { interval: 1, unit: 'day' },
time: '09:00',
timezone: 'America/New_York'
}
}
});
const savedSchedule = {
...scheduleData,
_id: 'sched123',
save: jest.fn().mockResolvedValue(scheduleData),
calculateNextRunTime: jest.fn().mockReturnValue(new Date())
};
ScheduledCampaign.mockImplementation(() => savedSchedule);
const result = await schedulerService.createSchedule(scheduleData);
expect(result.type).toBe('recurring');
expect(savedSchedule.calculateNextRunTime).toHaveBeenCalled();
});
it('should validate schedule data', async () => {
const invalidData = {
campaignId: 'camp123',
name: 'Test Schedule'
// Missing required fields
};
await expect(schedulerService.createSchedule(invalidData))
.rejects.toThrow('Schedule type is required');
});
it('should handle timezone conversion', async () => {
const scheduleData = createScheduledCampaign('camp123', {
type: 'recurring',
schedule: {
recurring: {
time: '09:00',
timezone: 'Europe/London'
}
}
});
const savedSchedule = {
...scheduleData,
_id: 'sched123',
save: jest.fn().mockResolvedValue(scheduleData)
};
ScheduledCampaign.mockImplementation(() => savedSchedule);
await schedulerService.createSchedule(scheduleData);
// Verify timezone handling
expect(savedSchedule.schedule.recurring.timezone).toBe('Europe/London');
});
});
describe('pauseSchedule', () => {
it('should pause active schedule', async () => {
const schedule = {
_id: 'sched123',
status: 'active',
save: jest.fn()
};
ScheduledCampaign.findById.mockResolvedValue(schedule);
await schedulerService.pauseSchedule('sched123');
expect(schedule.status).toBe('paused');
expect(schedule.save).toHaveBeenCalled();
});
it('should cancel pending jobs when pausing', async () => {
const schedule = {
_id: 'sched123',
status: 'active',
save: jest.fn()
};
const pendingJobs = [
{ _id: 'job1', status: 'pending', cancel: jest.fn() },
{ _id: 'job2', status: 'pending', cancel: jest.fn() }
];
ScheduledCampaign.findById.mockResolvedValue(schedule);
ScheduleJob.find.mockResolvedValue(pendingJobs);
await schedulerService.pauseSchedule('sched123');
expect(ScheduleJob.find).toHaveBeenCalledWith({
scheduleId: 'sched123',
status: 'pending'
});
expect(pendingJobs[0].cancel).toHaveBeenCalled();
expect(pendingJobs[1].cancel).toHaveBeenCalled();
});
it('should handle already paused schedule', async () => {
const schedule = {
_id: 'sched123',
status: 'paused'
};
ScheduledCampaign.findById.mockResolvedValue(schedule);
await expect(schedulerService.pauseSchedule('sched123'))
.rejects.toThrow('Schedule is already paused');
});
});
describe('resumeSchedule', () => {
it('should resume paused schedule', async () => {
const schedule = {
_id: 'sched123',
status: 'paused',
type: 'recurring',
campaignId: 'camp123',
save: jest.fn(),
calculateNextRunTime: jest.fn().mockReturnValue(new Date())
};
ScheduledCampaign.findById.mockResolvedValue(schedule);
await schedulerService.resumeSchedule('sched123');
expect(schedule.status).toBe('active');
expect(schedule.save).toHaveBeenCalled();
expect(schedule.calculateNextRunTime).toHaveBeenCalled();
expect(mockQueue.add).toHaveBeenCalled();
});
});
describe('processJob', () => {
it('should execute campaign for job', async () => {
const job = {
_id: 'job123',
scheduleId: 'sched123',
campaignId: 'camp123',
status: 'pending',
acquireLock: jest.fn().mockResolvedValue(true),
save: jest.fn()
};
const schedule = {
_id: 'sched123',
type: 'recurring',
recordExecution: jest.fn(),
calculateNextRunTime: jest.fn().mockReturnValue(new Date())
};
ScheduleJob.findById.mockResolvedValue(job);
ScheduledCampaign.findById.mockResolvedValue(schedule);
// Mock campaign execution
const mockCampaignExecution = jest.fn().mockResolvedValue({
success: true,
executionId: 'exec123'
});
schedulerService.executeCampaign = mockCampaignExecution;
await schedulerService.processJob('job123');
expect(job.acquireLock).toHaveBeenCalled();
expect(mockCampaignExecution).toHaveBeenCalledWith('camp123');
expect(job.status).toBe('completed');
expect(schedule.recordExecution).toHaveBeenCalled();
});
it('should handle job lock failure', async () => {
const job = {
_id: 'job123',
acquireLock: jest.fn().mockResolvedValue(false)
};
ScheduleJob.findById.mockResolvedValue(job);
await schedulerService.processJob('job123');
expect(job.acquireLock).toHaveBeenCalled();
expect(job.save).not.toHaveBeenCalled();
});
it('should handle campaign execution failure', async () => {
const job = {
_id: 'job123',
scheduleId: 'sched123',
campaignId: 'camp123',
status: 'pending',
attempts: 0,
acquireLock: jest.fn().mockResolvedValue(true),
save: jest.fn()
};
ScheduleJob.findById.mockResolvedValue(job);
// Mock campaign execution failure
const mockCampaignExecution = jest.fn().mockRejectedValue(new Error('Campaign failed'));
schedulerService.executeCampaign = mockCampaignExecution;
await schedulerService.processJob('job123');
expect(job.status).toBe('failed');
expect(job.lastError).toContain('Campaign failed');
expect(job.attempts).toBe(1);
});
it('should retry failed jobs', async () => {
const job = {
_id: 'job123',
scheduleId: 'sched123',
campaignId: 'camp123',
status: 'failed',
attempts: 1,
maxRetries: 3,
acquireLock: jest.fn().mockResolvedValue(true),
save: jest.fn()
};
ScheduleJob.findById.mockResolvedValue(job);
await schedulerService.processJob('job123');
expect(mockQueue.add).toHaveBeenCalledWith(
expect.objectContaining({ jobId: 'job123' }),
expect.objectContaining({ delay: expect.any(Number) })
);
});
});
describe('getSchedulePreview', () => {
it('should generate preview for recurring schedule', async () => {
const schedule = {
_id: 'sched123',
type: 'recurring',
schedule: {
recurring: {
pattern: 'weekly',
daysOfWeek: [1, 3, 5], // Mon, Wed, Fri
time: '10:00',
timezone: 'America/New_York'
}
}
};
ScheduledCampaign.findById.mockResolvedValue(schedule);
const preview = await schedulerService.getSchedulePreview('sched123', 5);
expect(preview).toHaveLength(5);
preview.forEach(item => {
expect(item).toHaveProperty('scheduledFor');
expect(item).toHaveProperty('dayOfWeek');
expect(item).toHaveProperty('localTime');
});
});
it('should handle one-time schedule preview', async () => {
const startDate = new Date('2024-06-01T14:00:00Z');
const schedule = {
_id: 'sched123',
type: 'one-time',
schedule: {
startDateTime: startDate
}
};
ScheduledCampaign.findById.mockResolvedValue(schedule);
const preview = await schedulerService.getSchedulePreview('sched123', 5);
expect(preview).toHaveLength(1);
expect(preview[0].scheduledFor).toEqual(startDate);
});
});
describe('deleteSchedule', () => {
it('should delete schedule and associated jobs', async () => {
const schedule = {
_id: 'sched123',
status: 'paused',
deleteOne: jest.fn()
};
ScheduledCampaign.findById.mockResolvedValue(schedule);
ScheduleJob.deleteMany.mockResolvedValue({ deletedCount: 3 });
await schedulerService.deleteSchedule('sched123');
expect(schedule.deleteOne).toHaveBeenCalled();
expect(ScheduleJob.deleteMany).toHaveBeenCalledWith({
scheduleId: 'sched123',
status: { $in: ['pending', 'failed'] }
});
});
it('should not delete active schedule', async () => {
const schedule = {
_id: 'sched123',
status: 'active'
};
ScheduledCampaign.findById.mockResolvedValue(schedule);
await expect(schedulerService.deleteSchedule('sched123'))
.rejects.toThrow('Cannot delete active schedule');
});
});
});