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:
239
marketing-agent/test/integration/api-gateway.test.js
Normal file
239
marketing-agent/test/integration/api-gateway.test.js
Normal file
@@ -0,0 +1,239 @@
|
||||
// API Gateway Integration Tests
|
||||
const TestEnvironment = require('./setup');
|
||||
const TestHelpers = require('./helpers');
|
||||
const apiGatewayApp = require('../../services/api-gateway/src/app');
|
||||
|
||||
describe('API Gateway Integration Tests', () => {
|
||||
let testEnv;
|
||||
let helpers;
|
||||
let apiClient;
|
||||
let testUser;
|
||||
let apiKey;
|
||||
const API_GATEWAY_PORT = 13000;
|
||||
const API_BASE_URL = `http://localhost:${API_GATEWAY_PORT}/api/v1`;
|
||||
|
||||
beforeAll(async () => {
|
||||
testEnv = new TestEnvironment();
|
||||
helpers = new TestHelpers(testEnv);
|
||||
|
||||
// Setup test environment
|
||||
await testEnv.setup();
|
||||
|
||||
// Setup mocks
|
||||
helpers.setupMocks();
|
||||
|
||||
// Start API Gateway
|
||||
await testEnv.startService('api-gateway', apiGatewayApp, API_GATEWAY_PORT);
|
||||
|
||||
// Create test user and API key
|
||||
testUser = await testEnv.createTestUser();
|
||||
apiKey = await testEnv.createTestApiKey(testUser.id);
|
||||
|
||||
// Create authenticated client
|
||||
apiClient = await helpers.createAuthenticatedClient(`http://localhost:${API_GATEWAY_PORT}`, testUser);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
helpers.cleanupMocks();
|
||||
await testEnv.cleanup();
|
||||
});
|
||||
|
||||
describe('Authentication', () => {
|
||||
test('should authenticate with JWT token', async () => {
|
||||
const response = await apiClient.get('/api/v1/auth/me');
|
||||
const data = helpers.expectApiSuccess(response);
|
||||
|
||||
expect(data.user).toBeDefined();
|
||||
expect(data.user.id).toBe(testUser.id);
|
||||
expect(data.user.username).toBe(testUser.username);
|
||||
});
|
||||
|
||||
test('should reject invalid token', async () => {
|
||||
const invalidClient = require('axios').create({
|
||||
baseURL: `http://localhost:${API_GATEWAY_PORT}`,
|
||||
headers: {
|
||||
'Authorization': 'Bearer invalid-token'
|
||||
},
|
||||
validateStatus: () => true
|
||||
});
|
||||
|
||||
const response = await invalidClient.get('/api/v1/auth/me');
|
||||
helpers.expectApiError(response, 401, 'Invalid token');
|
||||
});
|
||||
|
||||
test('should authenticate with API key', async () => {
|
||||
const apiKeyClient = require('axios').create({
|
||||
baseURL: `http://localhost:${API_GATEWAY_PORT}`,
|
||||
headers: {
|
||||
'X-API-Key': apiKey.apiKey
|
||||
},
|
||||
validateStatus: () => true
|
||||
});
|
||||
|
||||
const response = await apiKeyClient.get('/api/v1/auth/me');
|
||||
const data = helpers.expectApiSuccess(response);
|
||||
|
||||
expect(data.user).toBeDefined();
|
||||
expect(data.apiKey).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Rate Limiting', () => {
|
||||
test('should enforce rate limits', async () => {
|
||||
const requests = [];
|
||||
|
||||
// Make many requests quickly
|
||||
for (let i = 0; i < 105; i++) {
|
||||
requests.push(
|
||||
apiClient.get('/api/v1/health').catch(e => e.response)
|
||||
);
|
||||
}
|
||||
|
||||
const responses = await Promise.all(requests);
|
||||
|
||||
// Check that some requests were rate limited
|
||||
const rateLimited = responses.filter(r => r.status === 429);
|
||||
expect(rateLimited.length).toBeGreaterThan(0);
|
||||
|
||||
// Verify rate limit headers
|
||||
const limitedResponse = rateLimited[0];
|
||||
expect(limitedResponse.headers['x-ratelimit-limit']).toBeDefined();
|
||||
expect(limitedResponse.headers['x-ratelimit-remaining']).toBeDefined();
|
||||
expect(limitedResponse.headers['x-ratelimit-reset']).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Service Routing', () => {
|
||||
test('should route to orchestrator service', async () => {
|
||||
const campaign = await helpers.createTestCampaign();
|
||||
|
||||
const response = await apiClient.get(`/api/v1/campaigns/${campaign.campaignId}`);
|
||||
const data = helpers.expectApiSuccess(response);
|
||||
|
||||
expect(data.campaign).toBeDefined();
|
||||
expect(data.campaign.campaignId).toBe(campaign.campaignId);
|
||||
});
|
||||
|
||||
test('should route to analytics service', async () => {
|
||||
const response = await apiClient.get('/api/v1/analytics/metrics', {
|
||||
params: {
|
||||
startDate: new Date(Date.now() - 86400000).toISOString(),
|
||||
endDate: new Date().toISOString()
|
||||
}
|
||||
});
|
||||
|
||||
const data = helpers.expectApiSuccess(response);
|
||||
expect(data.metrics).toBeDefined();
|
||||
});
|
||||
|
||||
test('should handle service unavailable', async () => {
|
||||
// Try to access a non-existent service endpoint
|
||||
const response = await apiClient.get('/api/v1/nonexistent/endpoint');
|
||||
helpers.expectApiError(response, 404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CORS', () => {
|
||||
test('should handle CORS preflight requests', async () => {
|
||||
const axios = require('axios');
|
||||
const response = await axios.options(`http://localhost:${API_GATEWAY_PORT}/api/v1/health`, {
|
||||
headers: {
|
||||
'Origin': 'http://localhost:3008',
|
||||
'Access-Control-Request-Method': 'GET',
|
||||
'Access-Control-Request-Headers': 'authorization'
|
||||
},
|
||||
validateStatus: () => true
|
||||
});
|
||||
|
||||
expect(response.status).toBe(204);
|
||||
expect(response.headers['access-control-allow-origin']).toBe('http://localhost:3008');
|
||||
expect(response.headers['access-control-allow-methods']).toContain('GET');
|
||||
expect(response.headers['access-control-allow-headers']).toContain('authorization');
|
||||
});
|
||||
|
||||
test('should reject unauthorized origins', async () => {
|
||||
const axios = require('axios');
|
||||
const response = await axios.get(`http://localhost:${API_GATEWAY_PORT}/api/v1/health`, {
|
||||
headers: {
|
||||
'Origin': 'http://unauthorized.com'
|
||||
},
|
||||
validateStatus: () => true
|
||||
});
|
||||
|
||||
expect(response.headers['access-control-allow-origin']).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
test('should handle validation errors', async () => {
|
||||
const response = await apiClient.post('/api/v1/campaigns', {
|
||||
// Missing required fields
|
||||
name: 'Test Campaign'
|
||||
});
|
||||
|
||||
helpers.expectApiError(response, 400);
|
||||
expect(response.data.errors).toBeDefined();
|
||||
});
|
||||
|
||||
test('should handle internal server errors gracefully', async () => {
|
||||
// Force an error by sending invalid data type
|
||||
const response = await apiClient.post('/api/v1/campaigns', 'invalid-data-type');
|
||||
|
||||
helpers.expectApiError(response, 400);
|
||||
expect(response.data.error).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Health Checks', () => {
|
||||
test('should return health status', async () => {
|
||||
const response = await apiClient.get('/api/v1/health');
|
||||
const data = helpers.expectApiSuccess(response);
|
||||
|
||||
expect(data.status).toBe('healthy');
|
||||
expect(data.timestamp).toBeDefined();
|
||||
expect(data.version).toBeDefined();
|
||||
});
|
||||
|
||||
test('should return detailed service health', async () => {
|
||||
const response = await apiClient.get('/api/v1/health/services');
|
||||
const data = helpers.expectApiSuccess(response);
|
||||
|
||||
expect(data.services).toBeDefined();
|
||||
expect(data.services['api-gateway']).toBeDefined();
|
||||
expect(data.services['api-gateway'].status).toBe('healthy');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Request Logging', () => {
|
||||
test('should log requests with correlation ID', async () => {
|
||||
const correlationId = 'test-correlation-' + Date.now();
|
||||
|
||||
const response = await apiClient.get('/api/v1/health', {
|
||||
headers: {
|
||||
'X-Correlation-ID': correlationId
|
||||
}
|
||||
});
|
||||
|
||||
helpers.expectApiSuccess(response);
|
||||
expect(response.headers['x-correlation-id']).toBe(correlationId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('API Versioning', () => {
|
||||
test('should support multiple API versions', async () => {
|
||||
// Test v1
|
||||
const v1Response = await apiClient.get('/api/v1/health');
|
||||
helpers.expectApiSuccess(v1Response);
|
||||
|
||||
// Test v2 (if implemented)
|
||||
const v2Response = await apiClient.get('/api/v2/health');
|
||||
if (v2Response.status === 200) {
|
||||
const data = helpers.expectApiSuccess(v2Response);
|
||||
expect(data.apiVersion).toBe('v2');
|
||||
} else {
|
||||
// v2 not implemented yet
|
||||
expect(v2Response.status).toBe(404);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
365
marketing-agent/test/integration/compliance.test.js
Normal file
365
marketing-agent/test/integration/compliance.test.js
Normal file
@@ -0,0 +1,365 @@
|
||||
// Compliance Integration Tests
|
||||
const TestEnvironment = require('./setup');
|
||||
const TestHelpers = require('./helpers');
|
||||
const apiGatewayApp = require('../../services/api-gateway/src/app');
|
||||
const complianceGuardApp = require('../../services/compliance-guard/src/app');
|
||||
|
||||
describe('Compliance Integration Tests', () => {
|
||||
let testEnv;
|
||||
let helpers;
|
||||
let apiClient;
|
||||
let testUser;
|
||||
const SERVICES = {
|
||||
'api-gateway': { app: apiGatewayApp, port: 13000 },
|
||||
'compliance-guard': { app: complianceGuardApp, port: 13006 }
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
testEnv = new TestEnvironment();
|
||||
helpers = new TestHelpers(testEnv);
|
||||
|
||||
// Setup test environment
|
||||
await testEnv.setup();
|
||||
|
||||
// Setup mocks
|
||||
helpers.setupMocks();
|
||||
|
||||
// Start services
|
||||
for (const [name, config] of Object.entries(SERVICES)) {
|
||||
await testEnv.startService(name, config.app, config.port);
|
||||
}
|
||||
|
||||
// Create test user
|
||||
testUser = await testEnv.createTestUser();
|
||||
|
||||
// Create authenticated client
|
||||
apiClient = await helpers.createAuthenticatedClient(
|
||||
`http://localhost:${SERVICES['api-gateway'].port}`,
|
||||
testUser
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
helpers.cleanupMocks();
|
||||
await testEnv.cleanup();
|
||||
});
|
||||
|
||||
describe('Data Protection', () => {
|
||||
test('should encrypt sensitive data', async () => {
|
||||
const sensitiveData = {
|
||||
userId: testUser.id,
|
||||
data: {
|
||||
ssn: '123-45-6789',
|
||||
creditCard: '4111111111111111',
|
||||
email: 'test@example.com'
|
||||
}
|
||||
};
|
||||
|
||||
const response = await apiClient.post(
|
||||
'/api/v1/compliance/data/encrypt',
|
||||
sensitiveData
|
||||
);
|
||||
|
||||
const { encrypted } = helpers.expectApiSuccess(response);
|
||||
|
||||
expect(encrypted).toBeDefined();
|
||||
expect(encrypted.data).not.toEqual(sensitiveData.data);
|
||||
expect(encrypted.data).toContain('encrypted:');
|
||||
});
|
||||
|
||||
test('should decrypt data for authorized users', async () => {
|
||||
// First encrypt
|
||||
const originalData = {
|
||||
message: 'Secret message',
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
const encryptResponse = await apiClient.post(
|
||||
'/api/v1/compliance/data/encrypt',
|
||||
{ userId: testUser.id, data: originalData }
|
||||
);
|
||||
|
||||
const { encrypted } = helpers.expectApiSuccess(encryptResponse);
|
||||
|
||||
// Then decrypt
|
||||
const decryptResponse = await apiClient.post(
|
||||
'/api/v1/compliance/data/decrypt',
|
||||
{ userId: testUser.id, data: encrypted.data }
|
||||
);
|
||||
|
||||
const { decrypted } = helpers.expectApiSuccess(decryptResponse);
|
||||
|
||||
expect(decrypted).toEqual(originalData);
|
||||
});
|
||||
|
||||
test('should prevent unauthorized decryption', async () => {
|
||||
// Create another user
|
||||
const otherUser = await testEnv.createTestUser();
|
||||
const otherClient = await helpers.createAuthenticatedClient(
|
||||
`http://localhost:${SERVICES['api-gateway'].port}`,
|
||||
otherUser
|
||||
);
|
||||
|
||||
// Try to decrypt data encrypted by first user
|
||||
const response = await otherClient.post(
|
||||
'/api/v1/compliance/data/decrypt',
|
||||
{ userId: testUser.id, data: 'encrypted:somedata' }
|
||||
);
|
||||
|
||||
helpers.expectApiError(response, 403, 'Unauthorized');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Privacy Rights', () => {
|
||||
test('should handle data export request', async () => {
|
||||
// Create some test data
|
||||
await helpers.createTestCampaign({ userId: testUser.id });
|
||||
await helpers.createTestTask({ userId: testUser.id });
|
||||
|
||||
const response = await apiClient.post(
|
||||
'/api/v1/compliance/privacy/export',
|
||||
{ userId: testUser.id }
|
||||
);
|
||||
|
||||
const { exportId, status } = helpers.expectApiSuccess(response, 202);
|
||||
|
||||
expect(exportId).toBeDefined();
|
||||
expect(status).toBe('processing');
|
||||
|
||||
// Wait for export to complete
|
||||
await helpers.sleep(2000);
|
||||
|
||||
// Check export status
|
||||
const statusResponse = await apiClient.get(
|
||||
`/api/v1/compliance/privacy/export/${exportId}`
|
||||
);
|
||||
|
||||
const exportData = helpers.expectApiSuccess(statusResponse);
|
||||
expect(['completed', 'processing']).toContain(exportData.status);
|
||||
|
||||
if (exportData.status === 'completed') {
|
||||
expect(exportData.downloadUrl).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
test('should handle data deletion request', async () => {
|
||||
// Create test data
|
||||
const campaign = await helpers.createTestCampaign({ userId: testUser.id });
|
||||
|
||||
// Request deletion
|
||||
const response = await apiClient.post(
|
||||
'/api/v1/compliance/privacy/delete',
|
||||
{
|
||||
userId: testUser.id,
|
||||
scope: 'all',
|
||||
reason: 'User requested account deletion'
|
||||
}
|
||||
);
|
||||
|
||||
const { deletionId, status } = helpers.expectApiSuccess(response, 202);
|
||||
|
||||
expect(deletionId).toBeDefined();
|
||||
expect(status).toBe('scheduled');
|
||||
|
||||
// Wait for deletion
|
||||
await helpers.sleep(2000);
|
||||
|
||||
// Verify data was deleted
|
||||
const campaignResponse = await apiClient.get(
|
||||
`/api/v1/campaigns/${campaign.campaignId}`
|
||||
);
|
||||
|
||||
helpers.expectApiError(campaignResponse, 404);
|
||||
});
|
||||
|
||||
test('should respect data retention policies', async () => {
|
||||
const response = await apiClient.get('/api/v1/compliance/retention/policies');
|
||||
const { policies } = helpers.expectApiSuccess(response);
|
||||
|
||||
expect(policies).toBeDefined();
|
||||
expect(policies).toHaveProperty('messages');
|
||||
expect(policies).toHaveProperty('userdata');
|
||||
expect(policies).toHaveProperty('analytics');
|
||||
|
||||
expect(policies.messages.retentionDays).toBeGreaterThan(0);
|
||||
expect(policies.userdata.retentionDays).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Consent Management', () => {
|
||||
test('should record user consent', async () => {
|
||||
const consentData = {
|
||||
userId: testUser.id,
|
||||
purposes: [
|
||||
{
|
||||
purpose: 'marketing',
|
||||
granted: true,
|
||||
timestamp: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
purpose: 'analytics',
|
||||
granted: true,
|
||||
timestamp: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
purpose: 'personalization',
|
||||
granted: false,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
],
|
||||
ipAddress: '192.168.1.1',
|
||||
userAgent: 'Test Client/1.0'
|
||||
};
|
||||
|
||||
const response = await apiClient.post(
|
||||
'/api/v1/compliance/consent/record',
|
||||
consentData
|
||||
);
|
||||
|
||||
const { consentId } = helpers.expectApiSuccess(response, 201);
|
||||
|
||||
expect(consentId).toBeDefined();
|
||||
});
|
||||
|
||||
test('should check consent before processing', async () => {
|
||||
// Check consent status
|
||||
const response = await apiClient.get(
|
||||
`/api/v1/compliance/consent/check/${testUser.id}`,
|
||||
{
|
||||
params: { purpose: 'marketing' }
|
||||
}
|
||||
);
|
||||
|
||||
const { granted, timestamp } = helpers.expectApiSuccess(response);
|
||||
|
||||
expect(typeof granted).toBe('boolean');
|
||||
if (granted) {
|
||||
expect(timestamp).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
test('should update consent preferences', async () => {
|
||||
const updateData = {
|
||||
userId: testUser.id,
|
||||
updates: [
|
||||
{
|
||||
purpose: 'marketing',
|
||||
granted: false,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const response = await apiClient.put(
|
||||
'/api/v1/compliance/consent/update',
|
||||
updateData
|
||||
);
|
||||
|
||||
helpers.expectApiSuccess(response);
|
||||
|
||||
// Verify update
|
||||
const checkResponse = await apiClient.get(
|
||||
`/api/v1/compliance/consent/check/${testUser.id}`,
|
||||
{
|
||||
params: { purpose: 'marketing' }
|
||||
}
|
||||
);
|
||||
|
||||
const { granted } = helpers.expectApiSuccess(checkResponse);
|
||||
expect(granted).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Audit Logging', () => {
|
||||
test('should log compliance events', async () => {
|
||||
// Perform some compliance actions
|
||||
await apiClient.post('/api/v1/compliance/data/encrypt', {
|
||||
userId: testUser.id,
|
||||
data: { test: 'data' }
|
||||
});
|
||||
|
||||
await apiClient.get(`/api/v1/compliance/consent/check/${testUser.id}`, {
|
||||
params: { purpose: 'analytics' }
|
||||
});
|
||||
|
||||
// Get audit logs
|
||||
const response = await apiClient.get('/api/v1/compliance/audit/logs', {
|
||||
params: {
|
||||
userId: testUser.id,
|
||||
startDate: new Date(Date.now() - 3600000).toISOString(),
|
||||
endDate: new Date().toISOString()
|
||||
}
|
||||
});
|
||||
|
||||
const { logs, total } = helpers.expectApiSuccess(response);
|
||||
|
||||
expect(logs).toBeDefined();
|
||||
expect(logs.length).toBeGreaterThan(0);
|
||||
expect(total).toBeGreaterThan(0);
|
||||
|
||||
// Verify log structure
|
||||
const log = logs[0];
|
||||
expect(log).toHaveProperty('timestamp');
|
||||
expect(log).toHaveProperty('action');
|
||||
expect(log).toHaveProperty('userId');
|
||||
expect(log).toHaveProperty('details');
|
||||
});
|
||||
|
||||
test('should generate compliance reports', async () => {
|
||||
const response = await apiClient.post('/api/v1/compliance/audit/report', {
|
||||
type: 'monthly',
|
||||
month: new Date().getMonth() + 1,
|
||||
year: new Date().getFullYear()
|
||||
});
|
||||
|
||||
const { reportId, status } = helpers.expectApiSuccess(response, 202);
|
||||
|
||||
expect(reportId).toBeDefined();
|
||||
expect(status).toBe('generating');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Regulatory Compliance', () => {
|
||||
test('should validate GDPR compliance', async () => {
|
||||
const response = await apiClient.get('/api/v1/compliance/regulatory/gdpr/status');
|
||||
const { compliant, issues } = helpers.expectApiSuccess(response);
|
||||
|
||||
expect(typeof compliant).toBe('boolean');
|
||||
expect(Array.isArray(issues)).toBe(true);
|
||||
|
||||
if (!compliant) {
|
||||
expect(issues.length).toBeGreaterThan(0);
|
||||
issues.forEach(issue => {
|
||||
expect(issue).toHaveProperty('category');
|
||||
expect(issue).toHaveProperty('description');
|
||||
expect(issue).toHaveProperty('severity');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('should validate CCPA compliance', async () => {
|
||||
const response = await apiClient.get('/api/v1/compliance/regulatory/ccpa/status');
|
||||
const { compliant, issues } = helpers.expectApiSuccess(response);
|
||||
|
||||
expect(typeof compliant).toBe('boolean');
|
||||
expect(Array.isArray(issues)).toBe(true);
|
||||
});
|
||||
|
||||
test('should handle Do Not Sell requests', async () => {
|
||||
const response = await apiClient.post('/api/v1/compliance/privacy/donotsell', {
|
||||
userId: testUser.id,
|
||||
optOut: true
|
||||
});
|
||||
|
||||
helpers.expectApiSuccess(response);
|
||||
|
||||
// Verify opt-out status
|
||||
const statusResponse = await apiClient.get(
|
||||
`/api/v1/compliance/privacy/donotsell/${testUser.id}`
|
||||
);
|
||||
|
||||
const { optedOut } = helpers.expectApiSuccess(statusResponse);
|
||||
expect(optedOut).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
319
marketing-agent/test/integration/helpers.js
Normal file
319
marketing-agent/test/integration/helpers.js
Normal file
@@ -0,0 +1,319 @@
|
||||
// Test Helpers
|
||||
const jwt = require('jsonwebtoken');
|
||||
const axios = require('axios');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
|
||||
class TestHelpers {
|
||||
constructor(testEnv) {
|
||||
this.testEnv = testEnv;
|
||||
this.apiClient = null;
|
||||
}
|
||||
|
||||
// Create authenticated API client
|
||||
async createAuthenticatedClient(baseURL, user) {
|
||||
const token = this.generateJWT(user);
|
||||
|
||||
return axios.create({
|
||||
baseURL,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
validateStatus: () => true // Don't throw on any status
|
||||
});
|
||||
}
|
||||
|
||||
// Generate JWT token for testing
|
||||
generateJWT(user) {
|
||||
return jwt.sign(
|
||||
{
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
email: user.email
|
||||
},
|
||||
process.env.JWT_SECRET || 'test-jwt-secret',
|
||||
{ expiresIn: '1h' }
|
||||
);
|
||||
}
|
||||
|
||||
// Create test campaign
|
||||
async createTestCampaign(data = {}) {
|
||||
const defaultData = {
|
||||
campaignId: uuidv4(),
|
||||
name: `Test Campaign ${Date.now()}`,
|
||||
description: 'Test campaign for integration testing',
|
||||
targetAudience: {
|
||||
groups: ['test-group-1', 'test-group-2'],
|
||||
tags: ['test', 'automated'],
|
||||
filters: {}
|
||||
},
|
||||
messages: [
|
||||
{
|
||||
content: 'Test message content',
|
||||
type: 'text',
|
||||
delay: 0
|
||||
}
|
||||
],
|
||||
schedule: {
|
||||
startTime: new Date(),
|
||||
endTime: new Date(Date.now() + 86400000), // +1 day
|
||||
timezone: 'UTC'
|
||||
},
|
||||
goals: {
|
||||
impressions: 1000,
|
||||
clicks: 100,
|
||||
conversions: 10
|
||||
},
|
||||
status: 'draft'
|
||||
};
|
||||
|
||||
const campaign = { ...defaultData, ...data };
|
||||
|
||||
// Save to MongoDB
|
||||
const Campaign = require('../../services/orchestrator/src/models/Campaign');
|
||||
return await Campaign.create(campaign);
|
||||
}
|
||||
|
||||
// Create test task
|
||||
async createTestTask(data = {}) {
|
||||
const defaultData = {
|
||||
taskId: uuidv4(),
|
||||
type: 'SEND_MESSAGE',
|
||||
payload: {
|
||||
message: 'Test message',
|
||||
chatId: 'test-chat-123',
|
||||
options: {}
|
||||
},
|
||||
priority: 'medium',
|
||||
status: 'pending',
|
||||
retryCount: 0,
|
||||
maxRetries: 3
|
||||
};
|
||||
|
||||
const task = { ...defaultData, ...data };
|
||||
|
||||
// Save to MongoDB
|
||||
const Task = require('../../services/orchestrator/src/models/Task');
|
||||
return await Task.create(task);
|
||||
}
|
||||
|
||||
// Wait for condition with timeout
|
||||
async waitForCondition(checkFn, timeout = 5000, interval = 100) {
|
||||
const startTime = Date.now();
|
||||
|
||||
while (Date.now() - startTime < timeout) {
|
||||
if (await checkFn()) {
|
||||
return true;
|
||||
}
|
||||
await this.sleep(interval);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Sleep helper
|
||||
sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
// Mock external services
|
||||
setupMocks() {
|
||||
// Mock Telegram System API
|
||||
const nock = require('nock');
|
||||
|
||||
const telegramMock = nock(process.env.TELEGRAM_SYSTEM_URL || 'http://localhost:8080')
|
||||
.persist()
|
||||
.post('/api/messages/send')
|
||||
.reply(200, {
|
||||
success: true,
|
||||
messageId: uuidv4(),
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
.get('/api/accounts')
|
||||
.reply(200, {
|
||||
accounts: [
|
||||
{
|
||||
id: 'test-account-1',
|
||||
phone: '+1234567890',
|
||||
status: 'active'
|
||||
}
|
||||
]
|
||||
})
|
||||
.get('/api/groups')
|
||||
.reply(200, {
|
||||
groups: [
|
||||
{
|
||||
id: 'test-group-1',
|
||||
title: 'Test Group 1',
|
||||
members_count: 100
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// Mock Claude API
|
||||
const claudeMock = nock('https://api.anthropic.com')
|
||||
.persist()
|
||||
.post('/v1/messages')
|
||||
.reply(200, {
|
||||
id: 'msg_' + uuidv4(),
|
||||
type: 'message',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Mocked Claude response for testing'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// Mock OpenAI API
|
||||
const openaiMock = nock('https://api.openai.com')
|
||||
.persist()
|
||||
.post('/v1/moderations')
|
||||
.reply(200, {
|
||||
id: 'modr-' + uuidv4(),
|
||||
results: [
|
||||
{
|
||||
flagged: false,
|
||||
categories: {
|
||||
hate: false,
|
||||
harassment: false,
|
||||
violence: false
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
return { telegramMock, claudeMock, openaiMock };
|
||||
}
|
||||
|
||||
// Clean up mocks
|
||||
cleanupMocks() {
|
||||
const nock = require('nock');
|
||||
nock.cleanAll();
|
||||
}
|
||||
|
||||
// Verify API response
|
||||
expectApiSuccess(response, statusCode = 200) {
|
||||
expect(response.status).toBe(statusCode);
|
||||
expect(response.data).toBeDefined();
|
||||
if (response.data.error) {
|
||||
throw new Error(`API Error: ${response.data.error}`);
|
||||
}
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Verify API error
|
||||
expectApiError(response, statusCode, errorMessage) {
|
||||
expect(response.status).toBe(statusCode);
|
||||
if (errorMessage) {
|
||||
expect(response.data.error).toContain(errorMessage);
|
||||
}
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Create test webhook
|
||||
async createTestWebhook(url, events = ['message.sent', 'campaign.completed']) {
|
||||
return {
|
||||
id: uuidv4(),
|
||||
url,
|
||||
events,
|
||||
secret: 'test-webhook-secret',
|
||||
active: true,
|
||||
createdAt: new Date()
|
||||
};
|
||||
}
|
||||
|
||||
// Simulate webhook delivery
|
||||
async simulateWebhookDelivery(webhook, event, data) {
|
||||
const crypto = require('crypto');
|
||||
const payload = {
|
||||
id: uuidv4(),
|
||||
event,
|
||||
data,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
const signature = crypto
|
||||
.createHmac('sha256', webhook.secret)
|
||||
.update(JSON.stringify(payload))
|
||||
.digest('hex');
|
||||
|
||||
return axios.post(webhook.url, payload, {
|
||||
headers: {
|
||||
'X-Webhook-Signature': signature,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
validateStatus: () => true
|
||||
});
|
||||
}
|
||||
|
||||
// Generate test data
|
||||
generateTestData(type, count = 10) {
|
||||
const data = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
switch (type) {
|
||||
case 'messages':
|
||||
data.push({
|
||||
messageId: uuidv4(),
|
||||
content: `Test message ${i + 1}`,
|
||||
chatId: `chat-${i + 1}`,
|
||||
timestamp: new Date(Date.now() - i * 60000) // 1 min apart
|
||||
});
|
||||
break;
|
||||
|
||||
case 'events':
|
||||
data.push({
|
||||
eventId: uuidv4(),
|
||||
type: ['message.sent', 'message.delivered', 'message.read'][i % 3],
|
||||
data: { messageId: uuidv4() },
|
||||
timestamp: new Date()
|
||||
});
|
||||
break;
|
||||
|
||||
case 'metrics':
|
||||
data.push({
|
||||
timestamp: new Date(Date.now() - i * 3600000), // 1 hour apart
|
||||
impressions: Math.floor(Math.random() * 1000),
|
||||
clicks: Math.floor(Math.random() * 100),
|
||||
conversions: Math.floor(Math.random() * 10)
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// Verify database state
|
||||
async verifyDatabaseState(expectations) {
|
||||
const results = {};
|
||||
|
||||
if (expectations.mongodb) {
|
||||
for (const [collection, query] of Object.entries(expectations.mongodb)) {
|
||||
const Model = require(`../../services/orchestrator/src/models/${collection}`);
|
||||
results[collection] = await Model.find(query);
|
||||
}
|
||||
}
|
||||
|
||||
if (expectations.postgres) {
|
||||
for (const [table, query] of Object.entries(expectations.postgres)) {
|
||||
const result = await this.testEnv.pgPool.query(
|
||||
`SELECT * FROM ${table} WHERE ${query}`
|
||||
);
|
||||
results[table] = result.rows;
|
||||
}
|
||||
}
|
||||
|
||||
if (expectations.redis) {
|
||||
const redis = this.testEnv.getRedisClient();
|
||||
for (const key of expectations.redis) {
|
||||
results[`redis:${key}`] = await redis.get(key);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TestHelpers;
|
||||
408
marketing-agent/test/integration/orchestration.test.js
Normal file
408
marketing-agent/test/integration/orchestration.test.js
Normal file
@@ -0,0 +1,408 @@
|
||||
// Orchestration Integration Tests
|
||||
const TestEnvironment = require('./setup');
|
||||
const TestHelpers = require('./helpers');
|
||||
const apiGatewayApp = require('../../services/api-gateway/src/app');
|
||||
const orchestratorApp = require('../../services/orchestrator/src/app');
|
||||
const claudeAgentApp = require('../../services/claude-agent/src/app');
|
||||
const gramjsAdapterApp = require('../../services/gramjs-adapter/src/app');
|
||||
|
||||
describe('Orchestration Integration Tests', () => {
|
||||
let testEnv;
|
||||
let helpers;
|
||||
let apiClient;
|
||||
let testUser;
|
||||
const SERVICES = {
|
||||
'api-gateway': { app: apiGatewayApp, port: 13000 },
|
||||
'orchestrator': { app: orchestratorApp, port: 13001 },
|
||||
'claude-agent': { app: claudeAgentApp, port: 13002 },
|
||||
'gramjs-adapter': { app: gramjsAdapterApp, port: 13003 }
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
testEnv = new TestEnvironment();
|
||||
helpers = new TestHelpers(testEnv);
|
||||
|
||||
// Setup test environment
|
||||
await testEnv.setup();
|
||||
|
||||
// Setup mocks
|
||||
helpers.setupMocks();
|
||||
|
||||
// Start all services
|
||||
for (const [name, config] of Object.entries(SERVICES)) {
|
||||
await testEnv.startService(name, config.app, config.port);
|
||||
}
|
||||
|
||||
// Wait for services to be ready
|
||||
await helpers.sleep(2000);
|
||||
|
||||
// Create test user
|
||||
testUser = await testEnv.createTestUser();
|
||||
|
||||
// Create authenticated client
|
||||
apiClient = await helpers.createAuthenticatedClient(
|
||||
`http://localhost:${SERVICES['api-gateway'].port}`,
|
||||
testUser
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
helpers.cleanupMocks();
|
||||
await testEnv.cleanup();
|
||||
});
|
||||
|
||||
describe('Campaign Orchestration', () => {
|
||||
test('should create and execute a campaign', async () => {
|
||||
// Create campaign
|
||||
const campaignData = {
|
||||
name: 'Integration Test Campaign',
|
||||
description: 'Testing full orchestration flow',
|
||||
targetAudience: {
|
||||
groups: ['test-group-1'],
|
||||
tags: ['integration-test']
|
||||
},
|
||||
messages: [
|
||||
{
|
||||
content: 'Hello from integration test!',
|
||||
type: 'text',
|
||||
delay: 0
|
||||
},
|
||||
{
|
||||
content: 'This is the second message',
|
||||
type: 'text',
|
||||
delay: 1000
|
||||
}
|
||||
],
|
||||
schedule: {
|
||||
startTime: new Date(),
|
||||
endTime: new Date(Date.now() + 3600000),
|
||||
timezone: 'UTC'
|
||||
},
|
||||
goals: {
|
||||
impressions: 100,
|
||||
clicks: 10
|
||||
}
|
||||
};
|
||||
|
||||
const createResponse = await apiClient.post('/api/v1/campaigns', campaignData);
|
||||
const { campaign } = helpers.expectApiSuccess(createResponse, 201);
|
||||
|
||||
expect(campaign.campaignId).toBeDefined();
|
||||
expect(campaign.status).toBe('draft');
|
||||
|
||||
// Start campaign
|
||||
const startResponse = await apiClient.post(
|
||||
`/api/v1/campaigns/${campaign.campaignId}/start`
|
||||
);
|
||||
helpers.expectApiSuccess(startResponse);
|
||||
|
||||
// Wait for campaign to start processing
|
||||
await helpers.sleep(2000);
|
||||
|
||||
// Check campaign status
|
||||
const statusResponse = await apiClient.get(
|
||||
`/api/v1/campaigns/${campaign.campaignId}`
|
||||
);
|
||||
const updatedCampaign = helpers.expectApiSuccess(statusResponse).campaign;
|
||||
|
||||
expect(['active', 'completed']).toContain(updatedCampaign.status);
|
||||
|
||||
// Check that tasks were created
|
||||
const tasksResponse = await apiClient.get(
|
||||
`/api/v1/campaigns/${campaign.campaignId}/tasks`
|
||||
);
|
||||
const { tasks } = helpers.expectApiSuccess(tasksResponse);
|
||||
|
||||
expect(tasks).toBeDefined();
|
||||
expect(tasks.length).toBeGreaterThan(0);
|
||||
expect(tasks[0].type).toBe('SEND_MESSAGE');
|
||||
});
|
||||
|
||||
test('should handle campaign pause and resume', async () => {
|
||||
// Create and start campaign
|
||||
const campaign = await helpers.createTestCampaign({
|
||||
status: 'active'
|
||||
});
|
||||
|
||||
// Pause campaign
|
||||
const pauseResponse = await apiClient.post(
|
||||
`/api/v1/campaigns/${campaign.campaignId}/pause`
|
||||
);
|
||||
helpers.expectApiSuccess(pauseResponse);
|
||||
|
||||
// Verify paused status
|
||||
const pausedResponse = await apiClient.get(
|
||||
`/api/v1/campaigns/${campaign.campaignId}`
|
||||
);
|
||||
const pausedCampaign = helpers.expectApiSuccess(pausedResponse).campaign;
|
||||
expect(pausedCampaign.status).toBe('paused');
|
||||
|
||||
// Resume campaign
|
||||
const resumeResponse = await apiClient.post(
|
||||
`/api/v1/campaigns/${campaign.campaignId}/resume`
|
||||
);
|
||||
helpers.expectApiSuccess(resumeResponse);
|
||||
|
||||
// Verify active status
|
||||
const activeResponse = await apiClient.get(
|
||||
`/api/v1/campaigns/${campaign.campaignId}`
|
||||
);
|
||||
const activeCampaign = helpers.expectApiSuccess(activeResponse).campaign;
|
||||
expect(activeCampaign.status).toBe('active');
|
||||
});
|
||||
|
||||
test('should handle campaign cancellation', async () => {
|
||||
// Create and start campaign
|
||||
const campaign = await helpers.createTestCampaign({
|
||||
status: 'active'
|
||||
});
|
||||
|
||||
// Cancel campaign
|
||||
const cancelResponse = await apiClient.post(
|
||||
`/api/v1/campaigns/${campaign.campaignId}/cancel`
|
||||
);
|
||||
helpers.expectApiSuccess(cancelResponse);
|
||||
|
||||
// Verify cancelled status
|
||||
const cancelledResponse = await apiClient.get(
|
||||
`/api/v1/campaigns/${campaign.campaignId}`
|
||||
);
|
||||
const cancelledCampaign = helpers.expectApiSuccess(cancelledResponse).campaign;
|
||||
expect(cancelledCampaign.status).toBe('cancelled');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Task Execution', () => {
|
||||
test('should execute message sending task', async () => {
|
||||
// Create a message task
|
||||
const task = await helpers.createTestTask({
|
||||
type: 'SEND_MESSAGE',
|
||||
payload: {
|
||||
message: 'Test message from integration test',
|
||||
chatId: 'test-chat-123',
|
||||
options: {
|
||||
parseMode: 'HTML'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Execute task
|
||||
const executeResponse = await apiClient.post(
|
||||
`/api/v1/tasks/${task.taskId}/execute`
|
||||
);
|
||||
helpers.expectApiSuccess(executeResponse);
|
||||
|
||||
// Wait for execution
|
||||
await helpers.sleep(1000);
|
||||
|
||||
// Check task status
|
||||
const statusResponse = await apiClient.get(
|
||||
`/api/v1/tasks/${task.taskId}`
|
||||
);
|
||||
const executedTask = helpers.expectApiSuccess(statusResponse).task;
|
||||
|
||||
expect(['completed', 'processing']).toContain(executedTask.status);
|
||||
expect(executedTask.result).toBeDefined();
|
||||
});
|
||||
|
||||
test('should handle task retry on failure', async () => {
|
||||
// Create a task that will fail
|
||||
const task = await helpers.createTestTask({
|
||||
type: 'SEND_MESSAGE',
|
||||
payload: {
|
||||
message: 'Test message',
|
||||
chatId: 'invalid-chat-id', // This should cause a failure
|
||||
options: {}
|
||||
},
|
||||
maxRetries: 3
|
||||
});
|
||||
|
||||
// Execute task
|
||||
const executeResponse = await apiClient.post(
|
||||
`/api/v1/tasks/${task.taskId}/execute`
|
||||
);
|
||||
helpers.expectApiSuccess(executeResponse);
|
||||
|
||||
// Wait for retries
|
||||
await helpers.sleep(3000);
|
||||
|
||||
// Check task status
|
||||
const statusResponse = await apiClient.get(
|
||||
`/api/v1/tasks/${task.taskId}`
|
||||
);
|
||||
const retriedTask = helpers.expectApiSuccess(statusResponse).task;
|
||||
|
||||
expect(retriedTask.retryCount).toBeGreaterThan(0);
|
||||
expect(['failed', 'pending']).toContain(retriedTask.status);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Claude Agent Integration', () => {
|
||||
test('should generate campaign strategy using Claude', async () => {
|
||||
const strategyRequest = {
|
||||
goal: 'Increase user engagement',
|
||||
targetAudience: {
|
||||
demographics: {
|
||||
age: '25-35',
|
||||
interests: ['technology', 'startups']
|
||||
},
|
||||
size: 1000
|
||||
},
|
||||
constraints: {
|
||||
budget: 1000,
|
||||
duration: '7 days'
|
||||
}
|
||||
};
|
||||
|
||||
const response = await apiClient.post(
|
||||
'/api/v1/ai/strategy/generate',
|
||||
strategyRequest
|
||||
);
|
||||
|
||||
const { strategy } = helpers.expectApiSuccess(response);
|
||||
|
||||
expect(strategy).toBeDefined();
|
||||
expect(strategy.messages).toBeDefined();
|
||||
expect(strategy.schedule).toBeDefined();
|
||||
expect(strategy.expectedOutcomes).toBeDefined();
|
||||
});
|
||||
|
||||
test('should analyze campaign performance', async () => {
|
||||
// Create a completed campaign with some metrics
|
||||
const campaign = await helpers.createTestCampaign({
|
||||
status: 'completed',
|
||||
metrics: {
|
||||
impressions: 500,
|
||||
clicks: 50,
|
||||
conversions: 5
|
||||
}
|
||||
});
|
||||
|
||||
const analysisRequest = {
|
||||
campaignId: campaign.campaignId,
|
||||
metrics: campaign.metrics
|
||||
};
|
||||
|
||||
const response = await apiClient.post(
|
||||
'/api/v1/ai/analysis/campaign',
|
||||
analysisRequest
|
||||
);
|
||||
|
||||
const { analysis } = helpers.expectApiSuccess(response);
|
||||
|
||||
expect(analysis).toBeDefined();
|
||||
expect(analysis.performance).toBeDefined();
|
||||
expect(analysis.recommendations).toBeDefined();
|
||||
expect(analysis.insights).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Message Queue Integration', () => {
|
||||
test('should process messages through RabbitMQ', async () => {
|
||||
if (!testEnv.getRabbitChannel()) {
|
||||
console.log('Skipping RabbitMQ test - not available');
|
||||
return;
|
||||
}
|
||||
|
||||
const channel = testEnv.getRabbitChannel();
|
||||
const testQueue = 'test-integration-queue';
|
||||
const testMessage = {
|
||||
type: 'TEST_MESSAGE',
|
||||
data: {
|
||||
value: 'Integration test message',
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
};
|
||||
|
||||
// Create queue
|
||||
await channel.assertQueue(testQueue, { durable: false });
|
||||
|
||||
// Set up consumer
|
||||
const receivedMessages = [];
|
||||
await channel.consume(testQueue, (msg) => {
|
||||
if (msg) {
|
||||
receivedMessages.push(JSON.parse(msg.content.toString()));
|
||||
channel.ack(msg);
|
||||
}
|
||||
});
|
||||
|
||||
// Send message
|
||||
channel.sendToQueue(
|
||||
testQueue,
|
||||
Buffer.from(JSON.stringify(testMessage))
|
||||
);
|
||||
|
||||
// Wait for message to be processed
|
||||
await helpers.sleep(1000);
|
||||
|
||||
// Verify message was received
|
||||
expect(receivedMessages).toHaveLength(1);
|
||||
expect(receivedMessages[0]).toEqual(testMessage);
|
||||
|
||||
// Cleanup
|
||||
await channel.deleteQueue(testQueue);
|
||||
});
|
||||
});
|
||||
|
||||
describe('End-to-End Workflow', () => {
|
||||
test('should complete full marketing workflow', async () => {
|
||||
// 1. Generate strategy with Claude
|
||||
const strategyResponse = await apiClient.post('/api/v1/ai/strategy/generate', {
|
||||
goal: 'Product launch announcement',
|
||||
targetAudience: {
|
||||
demographics: { interests: ['tech'] },
|
||||
size: 500
|
||||
}
|
||||
});
|
||||
const { strategy } = helpers.expectApiSuccess(strategyResponse);
|
||||
|
||||
// 2. Create campaign based on strategy
|
||||
const campaignData = {
|
||||
name: 'Product Launch Campaign',
|
||||
description: 'Generated from AI strategy',
|
||||
targetAudience: strategy.targetAudience,
|
||||
messages: strategy.messages,
|
||||
schedule: strategy.schedule,
|
||||
goals: strategy.expectedOutcomes
|
||||
};
|
||||
|
||||
const createResponse = await apiClient.post('/api/v1/campaigns', campaignData);
|
||||
const { campaign } = helpers.expectApiSuccess(createResponse, 201);
|
||||
|
||||
// 3. Start campaign
|
||||
await apiClient.post(`/api/v1/campaigns/${campaign.campaignId}/start`);
|
||||
|
||||
// 4. Wait for some execution
|
||||
await helpers.sleep(3000);
|
||||
|
||||
// 5. Get analytics
|
||||
const analyticsResponse = await apiClient.get(
|
||||
`/api/v1/analytics/campaigns/${campaign.campaignId}/metrics`
|
||||
);
|
||||
const { metrics } = helpers.expectApiSuccess(analyticsResponse);
|
||||
|
||||
expect(metrics).toBeDefined();
|
||||
expect(metrics.totalMessages).toBeGreaterThanOrEqual(0);
|
||||
|
||||
// 6. Get AI analysis
|
||||
const analysisResponse = await apiClient.post('/api/v1/ai/analysis/campaign', {
|
||||
campaignId: campaign.campaignId,
|
||||
metrics: metrics
|
||||
});
|
||||
const { analysis } = helpers.expectApiSuccess(analysisResponse);
|
||||
|
||||
expect(analysis).toBeDefined();
|
||||
expect(analysis.performance).toBeDefined();
|
||||
|
||||
// 7. Stop campaign
|
||||
await apiClient.post(`/api/v1/campaigns/${campaign.campaignId}/cancel`);
|
||||
|
||||
// Verify final state
|
||||
const finalResponse = await apiClient.get(
|
||||
`/api/v1/campaigns/${campaign.campaignId}`
|
||||
);
|
||||
const finalCampaign = helpers.expectApiSuccess(finalResponse).campaign;
|
||||
expect(finalCampaign.status).toBe('cancelled');
|
||||
});
|
||||
});
|
||||
});
|
||||
241
marketing-agent/test/integration/setup.js
Normal file
241
marketing-agent/test/integration/setup.js
Normal file
@@ -0,0 +1,241 @@
|
||||
// Integration Test Setup
|
||||
const mongoose = require('mongoose');
|
||||
const { MongoMemoryServer } = require('mongodb-memory-server');
|
||||
const Redis = require('ioredis-mock');
|
||||
const amqp = require('amqplib');
|
||||
const { Pool } = require('pg');
|
||||
const { Client } = require('@elastic/elasticsearch');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
|
||||
class TestEnvironment {
|
||||
constructor() {
|
||||
this.mongoServer = null;
|
||||
this.redisClient = null;
|
||||
this.rabbitConnection = null;
|
||||
this.pgPool = null;
|
||||
this.esClient = null;
|
||||
this.servers = new Map();
|
||||
}
|
||||
|
||||
async setup() {
|
||||
console.log('Setting up test environment...');
|
||||
|
||||
// Setup MongoDB
|
||||
this.mongoServer = await MongoMemoryServer.create();
|
||||
const mongoUri = this.mongoServer.getUri();
|
||||
await mongoose.connect(mongoUri);
|
||||
|
||||
// Setup Redis Mock
|
||||
this.redisClient = new Redis({
|
||||
data: {}
|
||||
});
|
||||
|
||||
// Setup PostgreSQL (using test database)
|
||||
this.pgPool = new Pool({
|
||||
host: process.env.TEST_POSTGRES_HOST || 'localhost',
|
||||
port: process.env.TEST_POSTGRES_PORT || 5432,
|
||||
user: process.env.TEST_POSTGRES_USER || 'test_user',
|
||||
password: process.env.TEST_POSTGRES_PASSWORD || 'test_pass',
|
||||
database: process.env.TEST_POSTGRES_DB || 'marketing_test'
|
||||
});
|
||||
|
||||
// Create test database schema
|
||||
await this.setupPostgresSchema();
|
||||
|
||||
// Setup RabbitMQ connection
|
||||
if (process.env.TEST_RABBITMQ_URL) {
|
||||
try {
|
||||
this.rabbitConnection = await amqp.connect(process.env.TEST_RABBITMQ_URL);
|
||||
this.rabbitChannel = await this.rabbitConnection.createChannel();
|
||||
} catch (error) {
|
||||
console.warn('RabbitMQ not available for tests:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Setup Elasticsearch Mock
|
||||
this.esClient = {
|
||||
index: jest.fn().mockResolvedValue({ body: { _id: uuidv4() } }),
|
||||
search: jest.fn().mockResolvedValue({ body: { hits: { hits: [] } } }),
|
||||
bulk: jest.fn().mockResolvedValue({ body: { errors: false } }),
|
||||
ping: jest.fn().mockResolvedValue(true)
|
||||
};
|
||||
|
||||
// Set environment variables for services
|
||||
process.env.MONGODB_URI = mongoUri;
|
||||
process.env.REDIS_HOST = 'localhost';
|
||||
process.env.NODE_ENV = 'test';
|
||||
process.env.JWT_SECRET = 'test-jwt-secret-key-for-testing-only';
|
||||
process.env.ENCRYPTION_KEY = 'test-encryption-key-32-chars-long';
|
||||
|
||||
console.log('Test environment setup complete');
|
||||
}
|
||||
|
||||
async setupPostgresSchema() {
|
||||
const schemas = [
|
||||
// Users table
|
||||
`CREATE TABLE IF NOT EXISTS users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
username VARCHAR(255) UNIQUE NOT NULL,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)`,
|
||||
|
||||
// API Keys table
|
||||
`CREATE TABLE IF NOT EXISTS api_keys (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID REFERENCES users(id),
|
||||
key_hash VARCHAR(255) UNIQUE NOT NULL,
|
||||
name VARCHAR(255),
|
||||
permissions JSONB DEFAULT '[]',
|
||||
last_used_at TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
expires_at TIMESTAMP
|
||||
)`,
|
||||
|
||||
// Telegram Accounts table
|
||||
`CREATE TABLE IF NOT EXISTS telegram_accounts (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
phone_number VARCHAR(20) UNIQUE NOT NULL,
|
||||
api_id VARCHAR(255),
|
||||
api_hash VARCHAR(255),
|
||||
session_data TEXT,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)`
|
||||
];
|
||||
|
||||
for (const schema of schemas) {
|
||||
await this.pgPool.query(schema);
|
||||
}
|
||||
}
|
||||
|
||||
async startService(name, app, port) {
|
||||
return new Promise((resolve) => {
|
||||
const server = app.listen(port, () => {
|
||||
console.log(`Test ${name} service started on port ${port}`);
|
||||
this.servers.set(name, server);
|
||||
resolve(server);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async cleanup() {
|
||||
console.log('Cleaning up test environment...');
|
||||
|
||||
// Close all servers
|
||||
for (const [name, server] of this.servers) {
|
||||
await new Promise((resolve) => {
|
||||
server.close(resolve);
|
||||
});
|
||||
console.log(`Closed ${name} server`);
|
||||
}
|
||||
|
||||
// Cleanup databases
|
||||
if (mongoose.connection.readyState === 1) {
|
||||
await mongoose.disconnect();
|
||||
}
|
||||
|
||||
if (this.mongoServer) {
|
||||
await this.mongoServer.stop();
|
||||
}
|
||||
|
||||
if (this.pgPool) {
|
||||
await this.pgPool.end();
|
||||
}
|
||||
|
||||
if (this.rabbitConnection) {
|
||||
await this.rabbitChannel.close();
|
||||
await this.rabbitConnection.close();
|
||||
}
|
||||
|
||||
if (this.redisClient) {
|
||||
this.redisClient.disconnect();
|
||||
}
|
||||
|
||||
console.log('Test environment cleanup complete');
|
||||
}
|
||||
|
||||
// Helper methods for tests
|
||||
async createTestUser(userData = {}) {
|
||||
const defaultData = {
|
||||
username: `testuser_${Date.now()}`,
|
||||
email: `test_${Date.now()}@example.com`,
|
||||
password: 'TestPassword123!'
|
||||
};
|
||||
|
||||
const user = { ...defaultData, ...userData };
|
||||
|
||||
// Hash password
|
||||
const bcrypt = require('bcryptjs');
|
||||
const passwordHash = await bcrypt.hash(user.password, 10);
|
||||
|
||||
// Insert into database
|
||||
const result = await this.pgPool.query(
|
||||
'INSERT INTO users (username, email, password_hash) VALUES ($1, $2, $3) RETURNING *',
|
||||
[user.username, user.email, passwordHash]
|
||||
);
|
||||
|
||||
return {
|
||||
...result.rows[0],
|
||||
password: user.password // Return plain password for testing
|
||||
};
|
||||
}
|
||||
|
||||
async createTestApiKey(userId) {
|
||||
const crypto = require('crypto');
|
||||
const apiKey = `test_${crypto.randomBytes(16).toString('hex')}`;
|
||||
const keyHash = crypto.createHash('sha256').update(apiKey).digest('hex');
|
||||
|
||||
const result = await this.pgPool.query(
|
||||
'INSERT INTO api_keys (user_id, key_hash, name, permissions) VALUES ($1, $2, $3, $4) RETURNING *',
|
||||
[userId, keyHash, 'Test API Key', JSON.stringify(['read', 'write'])]
|
||||
);
|
||||
|
||||
return {
|
||||
...result.rows[0],
|
||||
apiKey // Return plain key for testing
|
||||
};
|
||||
}
|
||||
|
||||
async createTestTelegramAccount(data = {}) {
|
||||
const defaultData = {
|
||||
phone_number: `+1234567${Date.now().toString().slice(-4)}`,
|
||||
api_id: '12345',
|
||||
api_hash: 'test_api_hash',
|
||||
session_data: 'test_session_data'
|
||||
};
|
||||
|
||||
const account = { ...defaultData, ...data };
|
||||
|
||||
const result = await this.pgPool.query(
|
||||
'INSERT INTO telegram_accounts (phone_number, api_id, api_hash, session_data) VALUES ($1, $2, $3, $4) RETURNING *',
|
||||
[account.phone_number, account.api_id, account.api_hash, account.session_data]
|
||||
);
|
||||
|
||||
return result.rows[0];
|
||||
}
|
||||
|
||||
getRedisClient() {
|
||||
return this.redisClient;
|
||||
}
|
||||
|
||||
getMongoConnection() {
|
||||
return mongoose.connection;
|
||||
}
|
||||
|
||||
getPostgresPool() {
|
||||
return this.pgPool;
|
||||
}
|
||||
|
||||
getRabbitChannel() {
|
||||
return this.rabbitChannel;
|
||||
}
|
||||
|
||||
getElasticsearchClient() {
|
||||
return this.esClient;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TestEnvironment;
|
||||
Reference in New Issue
Block a user