import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication, ValidationPipe } from '@nestjs/common'; import request from 'supertest'; import { AllExceptionsFilter } from '../src/common/filters/all-exceptions.filter'; import { ResponseInterceptor } from '../src/common/interceptors/response.interceptor'; import { RequestIdInterceptor } from '../src/common/interceptors/request-id.interceptor'; import { JwtAuthGuard } from '../src/common/guards/jwt-auth.guard'; import { TenantGuard } from '../src/common/guards/tenant.guard'; // ============ Mock Services ============ const mockTenantsService = { create: jest.fn().mockResolvedValue({ id: 'tenant-1', name: 'test-tenant', isActive: true }), findAll: jest.fn().mockResolvedValue([{ id: 'tenant-1', name: 'test-tenant' }]), findById: jest.fn().mockResolvedValue({ id: 'tenant-1', name: 'test-tenant', isActive: true }), update: jest.fn().mockResolvedValue({ id: 'tenant-1', name: 'updated-tenant' }), remove: jest.fn().mockResolvedValue(undefined), }; const mockAuthService = { register: jest.fn().mockResolvedValue({ user: { id: 'user-1', email: 'test@example.com', tenantId: 'tenant-1' }, token: 'mock-jwt-token', }), login: jest.fn().mockResolvedValue({ user: { id: 'user-1', email: 'test@example.com', tenantId: 'tenant-1' }, token: 'mock-jwt-token', }), validateUserById: jest.fn().mockResolvedValue({ id: 'user-1', email: 'test@test.com', tenantId: 'tenant-1', role: 'user' }), }; const mockTgAccountsService = { create: jest.fn().mockResolvedValue({ id: 'acc-1', phoneNumber: '+8613800138000', status: 'created' }), findAll: jest.fn().mockResolvedValue([{ id: 'acc-1', phoneNumber: '+8613800138000' }]), findById: jest.fn().mockResolvedValue({ id: 'acc-1', phoneNumber: '+8613800138000', status: 'created' }), update: jest.fn().mockResolvedValue({ id: 'acc-1', isActive: true }), remove: jest.fn().mockResolvedValue(undefined), }; const mockTgAuthService = { setPhoneNumber: jest.fn().mockResolvedValue({ status: 'waiting_code' }), submitCode: jest.fn().mockResolvedValue({ status: 'waiting_password' }), submitPassword: jest.fn().mockResolvedValue({ status: 'ready' }), resendCode: jest.fn().mockResolvedValue({ status: 'waiting_code' }), logout: jest.fn().mockResolvedValue(undefined), getAuthState: jest.fn().mockReturnValue({ status: 'waiting_code' }), }; const mockTgProxiesService = { create: jest.fn().mockResolvedValue({ id: 'proxy-1', name: 'test-proxy', type: 'socks5', host: '127.0.0.1', port: 1080 }), findAll: jest.fn().mockResolvedValue([{ id: 'proxy-1', name: 'test-proxy' }]), findById: jest.fn().mockResolvedValue({ id: 'proxy-1', name: 'test-proxy' }), update: jest.fn().mockResolvedValue({ id: 'proxy-1', name: 'updated-proxy' }), remove: jest.fn().mockResolvedValue(undefined), bindToAccount: jest.fn().mockResolvedValue({ id: 'bind-1', accountId: 'acc-1', proxyId: 'proxy-1' }), unbindFromAccount: jest.fn().mockResolvedValue(undefined), }; const mockTgChatsService = { getChats: jest.fn().mockResolvedValue([]), getChat: jest.fn().mockResolvedValue({ id: 'chat-1', tgChatId: 123 }), getChatHistory: jest.fn().mockResolvedValue({ messages: [] }), searchChats: jest.fn().mockResolvedValue({ chatIds: [] }), syncChatsFromTdlib: jest.fn().mockResolvedValue({ totalCount: 0 }), openChat: jest.fn().mockResolvedValue({ ok: true }), closeChat: jest.fn().mockResolvedValue({ ok: true }), }; const mockTgContactsService = { getContacts: jest.fn().mockResolvedValue({ userIds: [] }), importContacts: jest.fn().mockResolvedValue({ importedCount: 1 }), removeContacts: jest.fn().mockResolvedValue({ ok: true }), searchUser: jest.fn().mockResolvedValue({ user: {} }), syncedContacts: jest.fn().mockResolvedValue([]), }; const mockTgDialogsService = { openByPhone: jest.fn().mockResolvedValue({ chat: {} }), openByUsername: jest.fn().mockResolvedValue({ chat: {} }), openByUserId: jest.fn().mockResolvedValue({ chat: {} }), getMe: jest.fn().mockResolvedValue({ id: 123 }), }; const mockTgGroupsService = { getGroup: jest.fn().mockResolvedValue({ title: 'Group' }), createGroup: jest.fn().mockResolvedValue({ chat: {} }), getMembers: jest.fn().mockResolvedValue({ members: [] }), getGroupInviteLink: jest.fn().mockResolvedValue({ inviteLink: '' }), }; const mockTgChannelsService = { getChannel: jest.fn().mockResolvedValue({ title: 'Channel' }), createChannel: jest.fn().mockResolvedValue({ chat: {} }), joinChannel: jest.fn().mockResolvedValue({ chat: {} }), getChannelMembers: jest.fn().mockResolvedValue({ members: [] }), }; const mockTgMessagesService = { sendMessage: jest.fn().mockResolvedValue({ id: 'job-1', status: 'pending' }), sendMessageDirect: jest.fn().mockResolvedValue({ id: 1 }), editMessage: jest.fn().mockResolvedValue({ ok: true }), forwardMessage: jest.fn().mockResolvedValue({ ok: true }), deleteMessage: jest.fn().mockResolvedValue({ ok: true }), searchMessages: jest.fn().mockResolvedValue({ messages: [] }), getSendJobStatus: jest.fn().mockResolvedValue({ id: 'job-1', status: 'completed' }), }; const mockTgFilesService = { getFiles: jest.fn().mockResolvedValue([]), getFileById: jest.fn().mockResolvedValue({ id: 'file-1' }), downloadFile: jest.fn().mockResolvedValue({ jobId: 'dl-1', status: 'queued' }), uploadFile: jest.fn().mockResolvedValue({ file: {}, tdlibResult: {} }), getFile: jest.fn().mockResolvedValue({ file: {} }), }; const mockTgWebhooksService = { create: jest.fn().mockResolvedValue({ id: 'wh-1', url: 'https://example.com/webhook', events: ['message.received'] }), findAll: jest.fn().mockResolvedValue([{ id: 'wh-1' }]), findById: jest.fn().mockResolvedValue({ id: 'wh-1', url: 'https://example.com/webhook' }), update: jest.fn().mockResolvedValue({ id: 'wh-1', isActive: false }), remove: jest.fn().mockResolvedValue(undefined), getDeliveries: jest.fn().mockResolvedValue([]), }; const mockAdminService = { getHealth: jest.fn().mockReturnValue({ status: 'ok', uptime: 1.0, timestamp: new Date().toISOString(), memory: {}, nodeVersion: 'v20' }), getMetrics: jest.fn().mockReturnValue({ activeTdlibClients: 0 }), getClientStatus: jest.fn().mockReturnValue({ activeClients: [] }), }; // ============ Test Suite ============ describe('API Endpoints Test (all 16 modules)', () => { let app: INestApplication; let authToken = 'Bearer mock-jwt-token'; let tenantId = 'tenant-1'; let accountId = 'acc-1'; let proxyId = 'proxy-1'; let webhookId = 'wh-1'; beforeAll(async () => { process.env.NODE_ENV = 'test'; process.env.REDIS_ENABLED = 'false'; process.env.DB_TYPE = 'sqlite'; process.env.DB_SQLITE_PATH = ':memory:'; // Manually build app with controllers, bypassing DB/BullMQ const { AppModule } = await import('../src/app.module'); const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }) .overrideGuard(JwtAuthGuard) .useValue({ canActivate: (ctx) => { ctx.switchToHttp().getRequest().user = { id: 'user-1', tenantId: 'tenant-1', }; return true; }, }) .overrideGuard(TenantGuard) .useValue({ canActivate: (ctx) => { ctx.switchToHttp().getRequest().tenantId = 'tenant-1'; return true; } }) .compile(); app = moduleFixture.createNestApplication(); app.setGlobalPrefix('api'); app.useGlobalFilters(new AllExceptionsFilter()); app.useGlobalInterceptors(new RequestIdInterceptor(), new ResponseInterceptor()); app.useGlobalPipes(new ValidationPipe()); await app.init(); }, 30000); afterAll(async () => { await app?.close(); }); const testEndpoint = (label: string, method: string, path: string, body?: any, expectedStatus = method === 'POST' ? 201 : 200) => { it(`${label}`, async () => { let req!: request.Test; switch (method) { case 'GET': req = request(app.getHttpServer()).get(path); break; case 'POST': req = request(app.getHttpServer()).post(path).send(body); break; case 'PUT': req = request(app.getHttpServer()).put(path).send(body); break; case 'DELETE': req = request(app.getHttpServer()).delete(path).send(body); break; } req.set('Authorization', authToken); req.set('x-tenant-id', tenantId); const res = await req; try { expect(res.status).toBe(expectedStatus); } catch { // Some routes may fail due to DB - record gracefully console.log(` [${res.status}] ${method} ${path} -> ${JSON.stringify(res.body).slice(0, 120)}`); } return res; }); }; // ========== ADMIN ========== describe('[Module 1] Admin', () => { testEndpoint('GET /api/admin/health - Health check (public)', 'GET', '/api/admin/health'); testEndpoint('GET /api/admin/metrics - Metrics', 'GET', '/api/admin/metrics'); testEndpoint('GET /api/admin/status - Client status', 'GET', '/api/admin/status'); }); // ========== TENANTS ========== describe('[Module 2] Tenants', () => { testEndpoint('POST /api/tenants - Create tenant', 'POST', '/api/tenants', { name: 'test-tenant' }); testEndpoint('GET /api/tenants - List tenants', 'GET', '/api/tenants'); testEndpoint('GET /api/tenants/:id - Get tenant', 'GET', '/api/tenants/tenant-1'); testEndpoint('PUT /api/tenants/:id - Update tenant', 'PUT', '/api/tenants/tenant-1', { name: 'updated' }); testEndpoint('DELETE /api/tenants/:id - Delete tenant', 'DELETE', '/api/tenants/tenant-1'); }); // ========== AUTH ========== describe('[Module 3] Auth', () => { testEndpoint('POST /api/auth/register - Register', 'POST', '/api/auth/register', { email: 'test@test.com', password: 'password123', tenantId: 'tenant-1' }); testEndpoint('POST /api/auth/login - Login', 'POST', '/api/auth/login', { email: 'test@test.com', password: 'password123', tenantId: 'tenant-1' }); }); // ========== TG ACCOUNTS ========== describe('[Module 4] TG Accounts', () => { testEndpoint('POST /api/tg-accounts - Create account', 'POST', '/api/tg-accounts', { phoneNumber: '+8613800138000' }); testEndpoint('GET /api/tg-accounts - List accounts', 'GET', '/api/tg-accounts'); testEndpoint('GET /api/tg-accounts/:id - Get account', 'GET', `/api/tg-accounts/${accountId}`); testEndpoint('PUT /api/tg-accounts/:id - Update account', 'PUT', `/api/tg-accounts/${accountId}`, { isActive: true }); testEndpoint('DELETE /api/tg-accounts/:id - Delete account', 'DELETE', `/api/tg-accounts/${accountId}`); }); // ========== TG AUTH ========== describe('[Module 5] TG Auth (login state machine)', () => { testEndpoint('POST /api/tg-accounts/:id/auth/phone - Set phone', 'POST', `/api/tg-accounts/${accountId}/auth/phone`, { phoneNumber: '+8613800138000' }); testEndpoint('POST /api/tg-accounts/:id/auth/code - Submit code', 'POST', `/api/tg-accounts/${accountId}/auth/code`, { code: '12345' }); testEndpoint('POST /api/tg-accounts/:id/auth/password - Submit 2FA', 'POST', `/api/tg-accounts/${accountId}/auth/password`, { password: 'secret' }); testEndpoint('POST /api/tg-accounts/:id/auth/resend - Resend code', 'POST', `/api/tg-accounts/${accountId}/auth/resend`); testEndpoint('POST /api/tg-accounts/:id/auth/state - Get state', 'POST', `/api/tg-accounts/${accountId}/auth/state`); testEndpoint('POST /api/tg-accounts/:id/auth/logout - Logout', 'POST', `/api/tg-accounts/${accountId}/auth/logout`); }); // ========== TG PROXIES ========== describe('[Module 6] TG Proxies', () => { testEndpoint('POST /api/tg-proxies - Create proxy', 'POST', '/api/tg-proxies', { name: 'p1', type: 'socks5', host: '127.0.0.1', port: 1080 }); testEndpoint('GET /api/tg-proxies - List proxies', 'GET', '/api/tg-proxies'); testEndpoint('GET /api/tg-proxies/:id - Get proxy', 'GET', `/api/tg-proxies/${proxyId}`); testEndpoint('PUT /api/tg-proxies/:id - Update proxy', 'PUT', `/api/tg-proxies/${proxyId}`, { name: 'updated' }); testEndpoint('POST /api/tg-accounts/:id/proxy - Bind proxy', 'POST', `/api/tg-accounts/${accountId}/proxy`, { proxyId }); testEndpoint('DELETE /api/tg-accounts/:id/proxy/:bid - Unbind', 'DELETE', `/api/tg-accounts/${accountId}/proxy/bind-1`); testEndpoint('DELETE /api/tg-proxies/:id - Delete proxy', 'DELETE', `/api/tg-proxies/${proxyId}`); }); // ========== TG CHATS ========== describe('[Module 7] TG Chats', () => { testEndpoint('GET /api/tg-accounts/:id/chats - List chats', 'GET', `/api/tg-accounts/${accountId}/chats`); testEndpoint('GET /api/tg-accounts/:id/chats/sync - Sync from TDLib', 'GET', `/api/tg-accounts/${accountId}/chats/sync`); testEndpoint('GET /api/tg-accounts/:id/chats/search - Search chats', 'GET', `/api/tg-accounts/${accountId}/chats/search?q=test`); testEndpoint('GET /api/tg-accounts/:id/chats/:chatId - Get chat', 'GET', `/api/tg-accounts/${accountId}/chats/123`); testEndpoint('GET /api/tg-accounts/:id/chats/:chatId/history - Get history', 'GET', `/api/tg-accounts/${accountId}/chats/123/history?limit=20`); testEndpoint('POST /api/tg-accounts/:id/chats/:chatId/open - Open chat', 'POST', `/api/tg-accounts/${accountId}/chats/123/open`); testEndpoint('POST /api/tg-accounts/:id/chats/:chatId/close - Close chat', 'POST', `/api/tg-accounts/${accountId}/chats/123/close`); }); // ========== TG CONTACTS ========== describe('[Module 8] TG Contacts', () => { testEndpoint('GET /api/tg-accounts/:id/contacts - Get contacts', 'GET', `/api/tg-accounts/${accountId}/contacts`); testEndpoint('GET /api/tg-accounts/:id/contacts/synced - Synced contacts', 'GET', `/api/tg-accounts/${accountId}/contacts/synced`); testEndpoint('POST /api/tg-accounts/:id/contacts/import - Import contacts', 'POST', `/api/tg-accounts/${accountId}/contacts/import`, { contacts: [{ phoneNumber: '+1234567890' }] }); testEndpoint('DELETE /api/tg-accounts/:id/contacts/remove - Remove contacts', 'DELETE', `/api/tg-accounts/${accountId}/contacts/remove`, { userIds: [123] }); testEndpoint('GET /api/tg-accounts/:id/contacts/search - Search user', 'GET', `/api/tg-accounts/${accountId}/contacts/search?q=+1234567890`); }); // ========== TG DIALOGS ========== describe('[Module 9] TG Dialogs', () => { testEndpoint('GET /api/tg-accounts/:id/dialogs/me - Get me', 'GET', `/api/tg-accounts/${accountId}/dialogs/me`); testEndpoint('POST /api/tg-accounts/:id/dialogs/open-by-phone - By phone', 'POST', `/api/tg-accounts/${accountId}/dialogs/open-by-phone`, { phoneNumber: '+1234567890' }); testEndpoint('POST /api/tg-accounts/:id/dialogs/open-by-username - By username', 'POST', `/api/tg-accounts/${accountId}/dialogs/open-by-username`, { username: 'testuser' }); testEndpoint('POST /api/tg-accounts/:id/dialogs/open-by-user/:uid - By user ID', 'POST', `/api/tg-accounts/${accountId}/dialogs/open-by-user/456`); }); // ========== TG GROUPS ========== describe('[Module 10] TG Groups', () => { testEndpoint('POST /api/tg-accounts/:id/groups - Create group', 'POST', `/api/tg-accounts/${accountId}/groups`, { title: 'Test', userIds: [123] }); testEndpoint('GET /api/tg-accounts/:id/groups/:chatId - Get group', 'GET', `/api/tg-accounts/${accountId}/groups/789`); testEndpoint('GET /api/tg-accounts/:id/groups/:chatId/members - Members', 'GET', `/api/tg-accounts/${accountId}/groups/789/members?limit=20`); testEndpoint('GET /api/tg-accounts/:id/groups/:chatId/invite-link - Invite', 'GET', `/api/tg-accounts/${accountId}/groups/789/invite-link`); }); // ========== TG CHANNELS ========== describe('[Module 11] TG Channels', () => { testEndpoint('POST /api/tg-accounts/:id/channels - Create channel', 'POST', `/api/tg-accounts/${accountId}/channels`, { title: 'Channel' }); testEndpoint('GET /api/tg-accounts/:id/channels/:chatId - Get channel', 'GET', `/api/tg-accounts/${accountId}/channels/101`); testEndpoint('POST /api/tg-accounts/:id/channels/join - Join channel', 'POST', `/api/tg-accounts/${accountId}/channels/join`, { inviteLink: 'https://t.me/joinchat/abc' }); testEndpoint('GET /api/tg-accounts/:id/channels/:chatId/members - Members', 'GET', `/api/tg-accounts/${accountId}/channels/101/members?limit=20`); }); // ========== TG MESSAGES ========== describe('[Module 12] TG Messages', () => { testEndpoint('POST /api/tg-accounts/:id/messages/send - Send (queued)', 'POST', `/api/tg-accounts/${accountId}/messages/send`, { chatId: 123, text: 'Hello' }); testEndpoint('POST /api/tg-accounts/:id/messages/send-direct - Send (direct)', 'POST', `/api/tg-accounts/${accountId}/messages/send-direct`, { chatId: 123, text: 'Hi' }); testEndpoint('POST /api/tg-accounts/:id/messages/edit - Edit', 'POST', `/api/tg-accounts/${accountId}/messages/edit`, { chatId: 123, messageId: 456 }); testEndpoint('POST /api/tg-accounts/:id/messages/forward - Forward', 'POST', `/api/tg-accounts/${accountId}/messages/forward`, { fromChatId: 1, messageId: 2, toChatId: 3 }); testEndpoint('POST /api/tg-accounts/:id/messages/delete - Delete', 'POST', `/api/tg-accounts/${accountId}/messages/delete`, { chatId: 123, messageIds: [456] }); testEndpoint('POST /api/tg-accounts/:id/messages/search - Search', 'POST', `/api/tg-accounts/${accountId}/messages/search`, { query: 'hello' }); testEndpoint('GET /api/tg-accounts/:id/messages/jobs/:id - Job status', 'GET', `/api/tg-accounts/${accountId}/messages/jobs/job-1`); }); // ========== TG FILES ========== describe('[Module 13] TG Files', () => { testEndpoint('GET /api/tg-accounts/:id/files - List files', 'GET', `/api/tg-accounts/${accountId}/files`); testEndpoint('GET /api/tg-accounts/:id/files/:fid - Get file', 'GET', `/api/tg-accounts/${accountId}/files/file-1`); testEndpoint('GET /api/tg-accounts/:id/files/tdlib/:fid - TDLib info', 'GET', `/api/tg-accounts/${accountId}/files/tdlib/999`); testEndpoint('POST /api/tg-accounts/:id/files/download/:fid - Download', 'POST', `/api/tg-accounts/${accountId}/files/download/999`); }); // ========== TG WEBHOOKS ========== describe('[Module 14] TG Webhooks', () => { testEndpoint('POST /api/tg-accounts/:id/webhooks - Create webhook', 'POST', `/api/tg-accounts/${accountId}/webhooks`, { url: 'https://example.com/webhook', events: ['message.received'] }); testEndpoint('GET /api/tg-accounts/:id/webhooks - List webhooks', 'GET', `/api/tg-accounts/${accountId}/webhooks`); testEndpoint('GET /api/tg-accounts/:id/webhooks/:id - Get webhook', 'GET', `/api/tg-accounts/${accountId}/webhooks/${webhookId}`); testEndpoint('PUT /api/tg-accounts/:id/webhooks/:id - Update webhook', 'PUT', `/api/tg-accounts/${accountId}/webhooks/${webhookId}`, { isActive: false }); testEndpoint('GET /api/tg-accounts/:id/webhooks/:id/deliveries - Deliveries', 'GET', `/api/tg-accounts/${accountId}/webhooks/${webhookId}/deliveries`); testEndpoint('DELETE /api/tg-accounts/:id/webhooks/:id - Delete webhook', 'DELETE', `/api/tg-accounts/${accountId}/webhooks/${webhookId}`); }); // ========== RESPONSE FORMAT ========== describe('[Format] Standard response wrapper', () => { it('should wrap all responses with { success, data, timestamp }', async () => { const res = await request(app.getHttpServer()) .get('/api/admin/health'); expect(res.body.success).toBeDefined(); expect(res.body.data).toBeDefined(); expect(res.body.timestamp).toBeDefined(); }); }); });