/** * Full API endpoint routing & validation test * Tests every route's registration, validation, response format, and error handling. * Uses mocked services - no DB/Redis required. */ import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication, Controller, Get, Post, Put, Delete, Body, Param, Query, UseGuards, SetMetadata, ValidationPipe, } from '@nestjs/common'; import request from 'supertest'; import { APP_GUARD } from '@nestjs/core'; 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 { TenantGuard } from '../src/common/guards/tenant.guard'; // ============ Build controllers directly (no DB) ============ // For testing, we build a simple module with all controllers // and mock their services. This validates routing, validation, auth guards. function Public() { return SetMetadata('isPublic', true); } // --- Mock Services --- const makeMock = (methods: string[]) => { const mock: any = {}; for (const m of methods) mock[m] = jest.fn().mockResolvedValue({ ok: true }); return mock; }; const mockTenantsService = makeMock(['create','findAll','findById','update','remove']); const mockAuthService = makeMock(['register','login','validateUserById']); const mockTgAccountsService = makeMock(['create','findAll','findById','update','remove']); const mockTgAuthService = makeMock(['setPhoneNumber','submitCode','submitPassword','resendCode','logout','getAuthState']); const mockTgProxiesService = makeMock(['create','findAll','findById','update','remove','bindToAccount','unbindFromAccount']); const mockTgChatsService = makeMock(['getChats','getChat','getChatHistory','searchChats','syncChatsFromTdlib','openChat','closeChat']); const mockTgContactsService = makeMock(['getContacts','importContacts','removeContacts','searchUser','syncedContacts']); const mockTgDialogsService = makeMock(['openByPhone','openByUsername','openByUserId','getMe']); const mockTgGroupsService = makeMock(['getGroup','createGroup','getMembers','getGroupInviteLink']); const mockTgChannelsService = makeMock(['getChannel','createChannel','joinChannel','getChannelMembers']); const mockTgMessagesService = makeMock(['sendMessage','sendMessageDirect','editMessage','forwardMessage','deleteMessage','searchMessages','getSendJobStatus']); const mockTgFilesService = makeMock(['getFiles','getFileById','downloadFile','uploadFile','getFile']); const mockTgWebhooksService = makeMock(['create','findAll','findById','update','remove','getDeliveries']); const mockAdminService = { getHealth: jest.fn().mockReturnValue({ status:'ok' }), getMetrics: jest.fn().mockReturnValue({}), getClientStatus: jest.fn().mockReturnValue({}) }; // --- Import real controllers --- import { TenantsController } from '../src/modules/tenants/tenants.controller'; import { AuthController } from '../src/modules/auth/auth.controller'; import { TgAccountsController } from '../src/modules/tg-accounts/tg-accounts.controller'; import { TgAuthController } from '../src/modules/tg-auth/tg-auth.controller'; import { TgProxiesController } from '../src/modules/tg-proxies/tg-proxies.controller'; import { TgChatsController } from '../src/modules/tg-chats/tg-chats.controller'; import { TgContactsController } from '../src/modules/tg-contacts/tg-contacts.controller'; import { TgDialogsController } from '../src/modules/tg-dialogs/tg-dialogs.controller'; import { TgGroupsController } from '../src/modules/tg-groups/tg-groups.controller'; import { TgChannelsController } from '../src/modules/tg-channels/tg-channels.controller'; import { TgMessagesController } from '../src/modules/tg-messages/tg-messages.controller'; import { TgFilesController } from '../src/modules/tg-files/tg-files.controller'; import { TgWebhooksController } from '../src/modules/tg-webhooks/tg-webhooks.controller'; import { AdminController } from '../src/modules/admin/admin.controller'; // --- Import real services (as tokens) --- import { AdminService } from '../src/modules/admin/admin.service'; import { TenantsService } from '../src/modules/tenants/tenants.service'; import { AuthService } from '../src/modules/auth/auth.service'; import { TgAccountsService } from '../src/modules/tg-accounts/tg-accounts.service'; import { TgAuthService } from '../src/modules/tg-auth/tg-auth.service'; import { TgProxiesService } from '../src/modules/tg-proxies/tg-proxies.service'; import { TgChatsService } from '../src/modules/tg-chats/tg-chats.service'; import { TgContactsService } from '../src/modules/tg-contacts/tg-contacts.service'; import { TgDialogsService } from '../src/modules/tg-dialogs/tg-dialogs.service'; import { TgGroupsService } from '../src/modules/tg-groups/tg-groups.service'; import { TgChannelsService } from '../src/modules/tg-channels/tg-channels.service'; import { TgMessagesService } from '../src/modules/tg-messages/tg-messages.service'; import { TgFilesService } from '../src/modules/tg-files/tg-files.service'; import { TgWebhooksService } from '../src/modules/tg-webhooks/tg-webhooks.service'; @Controller() class FakeController {} // Mock AuthGuard: always passes, sets user class MockJwtAuthGuard { canActivate(ctx: any) { const req = ctx.switchToHttp().getRequest(); req.user = { id: 'user-1', email: 'test@test.com', tenantId: 'tenant-1', role: 'admin' }; return true; }} // Mock TenantGuard: always passes class MockTenantGuard { canActivate(ctx: any) { ctx.switchToHttp().getRequest().tenantId = 'tenant-1'; return true; }} describe('=== Telegram User Gateway - Full API Endpoint Test ===', () => { let app: INestApplication; const accountId = 'acc-test-1'; beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [ AdminController, TenantsController, AuthController, TgAccountsController, TgAuthController, TgProxiesController, TgChatsController, TgContactsController, TgDialogsController, TgGroupsController, TgChannelsController, TgMessagesController, TgFilesController, TgWebhooksController, ], providers: [ { provide: AdminService, useValue: mockAdminService }, { provide: TenantsService, useValue: mockTenantsService }, { provide: AuthService, useValue: mockAuthService }, { provide: TgAccountsService, useValue: mockTgAccountsService }, { provide: TgAuthService, useValue: mockTgAuthService }, { provide: TgProxiesService, useValue: mockTgProxiesService }, { provide: TgChatsService, useValue: mockTgChatsService }, { provide: TgContactsService, useValue: mockTgContactsService }, { provide: TgDialogsService, useValue: mockTgDialogsService }, { provide: TgGroupsService, useValue: mockTgGroupsService }, { provide: TgChannelsService, useValue: mockTgChannelsService }, { provide: TgMessagesService, useValue: mockTgMessagesService }, { provide: TgFilesService, useValue: mockTgFilesService }, { provide: TgWebhooksService, useValue: mockTgWebhooksService }, ], }) .overrideGuard(TenantGuard).useClass(MockTenantGuard) .overrideGuard(require('../src/common/guards/jwt-auth.guard').JwtAuthGuard).useClass(MockJwtAuthGuard) .compile(); app = module.createNestApplication(); app.setGlobalPrefix('api'); app.useGlobalFilters(new AllExceptionsFilter()); app.useGlobalInterceptors(new RequestIdInterceptor(), new ResponseInterceptor()); app.useGlobalPipes(new ValidationPipe()); await app.init(); }); afterAll(async () => { await app?.close(); }); // Helper to do authenticated requests const req = (method: string, path: string, body?: any) => { const s = request(app.getHttpServer()); let r: request.Test; switch (method) { case 'GET': r = s.get(path); break; case 'POST': r = s.post(path); break; case 'PUT': r = s.put(path); break; case 'DELETE': r = s.delete(path); break; default: r = s.get(path); } r.set('Authorization', 'Bearer test-token'); r.set('x-tenant-id', 'tenant-1'); if (body && method !== 'GET') r.send(body); return r; }; // ===================================================== // MODULE 1: Admin (3 endpoints) // ===================================================== describe('[1] Admin Module', () => { it('GET /api/admin/health (public)', () => request(app.getHttpServer()).get('/api/admin/health').expect(200)); it('GET /api/admin/metrics', () => req('GET', '/api/admin/metrics').expect(200)); it('GET /api/admin/status', () => req('GET', '/api/admin/status').expect(200)); }); // ===================================================== // MODULE 2: Tenants (5 endpoints) // ===================================================== describe('[2] Tenants Module', () => { it('POST /api/tenants Create (public, validates)', async () => { await request(app.getHttpServer()).post('/api/tenants').send({}).expect(400); // missing name await request(app.getHttpServer()).post('/api/tenants').send({ name: 't1' }).expect(201); }); it('GET /api/tenants List', () => req('GET','/api/tenants').expect(200)); it('GET /api/tenants/:id Get by ID', () => req('GET','/api/tenants/t-1').expect(200)); it('PUT /api/tenants/:id Update', () => req('PUT','/api/tenants/t-1', { name:'x' }).expect(200)); it('DELETE /api/tenants/:id Remove', () => req('DELETE','/api/tenants/t-1').expect(200)); }); // ===================================================== // MODULE 3: Auth (2 endpoints) // ===================================================== describe('[3] Auth Module', () => { it('POST /api/auth/register (public, validates)', async () => { mockAuthService.register.mockResolvedValue({ user:{}, token:'tok' }); await request(app.getHttpServer()).post('/api/auth/register').send({}).expect(400); // no fields await request(app.getHttpServer()).post('/api/auth/register') .send({ email:'a@b.com', password:'12345678', tenantId:'t1' }).expect(201); }); it('POST /api/auth/login (public, validates)', async () => { mockAuthService.login.mockResolvedValue({ user:{}, token:'tok' }); await request(app.getHttpServer()).post('/api/auth/login').send({}).expect(400); // no fields await request(app.getHttpServer()).post('/api/auth/login') .send({ email:'a@b.com', password:'12345678', tenantId:'t1' }).expect(201); }); }); // ===================================================== // MODULE 4: TG Accounts (5 endpoints) // ===================================================== describe('[4] TG Accounts Module', () => { it('POST /api/tg-accounts Create (validates)', async () => { mockTgAccountsService.create.mockResolvedValue({ id:accountId, phoneNumber:'+86' }); await req('POST','/api/tg-accounts', {}).expect(400); // no phoneNumber await req('POST','/api/tg-accounts', { phoneNumber:'+8613800138000' }).expect(201); }); it('GET /api/tg-accounts List', () => req('GET','/api/tg-accounts').expect(200)); it('GET /api/tg-accounts/:id Get', () => req('GET',`/api/tg-accounts/${accountId}`).expect(200)); it('PUT /api/tg-accounts/:id Update', () => req('PUT',`/api/tg-accounts/${accountId}`, { isActive:true }).expect(200)); it('DELETE /api/tg-accounts/:id Remove', () => req('DELETE',`/api/tg-accounts/${accountId}`).expect(200)); }); // ===================================================== // MODULE 5: TG Auth (6 endpoints) // ===================================================== describe('[5] TG Auth Module (Login State Machine)', () => { it('POST /tg-accounts/:id/auth/phone Set phone', () => req('POST',`/api/tg-accounts/${accountId}/auth/phone`, { phoneNumber:'+86' }).expect(201)); it('POST /tg-accounts/:id/auth/code Submit code', () => req('POST',`/api/tg-accounts/${accountId}/auth/code`, { code:'12345' }).expect(201)); it('POST /tg-accounts/:id/auth/password Submit 2FA password', () => req('POST',`/api/tg-accounts/${accountId}/auth/password`, { password:'pass' }).expect(201)); it('POST /tg-accounts/:id/auth/resend Resend code', () => req('POST',`/api/tg-accounts/${accountId}/auth/resend`).expect(201)); it('POST /tg-accounts/:id/auth/state Get auth state', () => req('POST',`/api/tg-accounts/${accountId}/auth/state`).expect(201)); it('POST /tg-accounts/:id/auth/logout Logout', () => req('POST',`/api/tg-accounts/${accountId}/auth/logout`).expect(201)); }); // ===================================================== // MODULE 6: TG Proxies (7 endpoints) // ===================================================== describe('[6] TG Proxies Module', () => { const proxyId = 'proxy-test-1'; it('POST /api/tg-proxies Create (validates)', async () => { await req('POST','/api/tg-proxies', {}).expect(400); mockTgProxiesService.create.mockResolvedValue({ id:proxyId }); await req('POST','/api/tg-proxies', { name:'p', type:'socks5', host:'1.1.1.1', port:1080 }).expect(201); }); it('GET /api/tg-proxies List', () => req('GET','/api/tg-proxies').expect(200)); it('GET /api/tg-proxies/:id Get', () => req('GET',`/api/tg-proxies/${proxyId}`).expect(200)); it('PUT /api/tg-proxies/:id Update', () => req('PUT',`/api/tg-proxies/${proxyId}`, { name:'x' }).expect(200)); it('POST /api/tg-accounts/:id/proxy Bind', () => req('POST',`/api/tg-accounts/${accountId}/proxy`, { proxyId }).expect(201)); it('DELETE /api/tg-accounts/:id/proxy/:bid Unbind', () => req('DELETE',`/api/tg-accounts/${accountId}/proxy/${proxyId}`).expect(200)); it('DELETE /api/tg-proxies/:id Delete', () => req('DELETE',`/api/tg-proxies/${proxyId}`).expect(200)); }); // ===================================================== // MODULE 7: TG Chats (7 endpoints) // ===================================================== describe('[7] TG Chats Module', () => { it('GET /tg-accounts/:id/chats List', () => req('GET',`/api/tg-accounts/${accountId}/chats`).expect(200)); it('GET /tg-accounts/:id/chats/sync Sync', () => req('GET',`/api/tg-accounts/${accountId}/chats/sync`).expect(200)); it('GET /tg-accounts/:id/chats/search Search', () => req('GET',`/api/tg-accounts/${accountId}/chats/search?q=hi`).expect(200)); it('GET /tg-accounts/:id/chats/:chatId Detail', () => req('GET',`/api/tg-accounts/${accountId}/chats/999`).expect(200)); it('GET /tg-accounts/:id/chats/:chatId/history History', () => req('GET',`/api/tg-accounts/${accountId}/chats/999/history`).expect(200)); it('POST /tg-accounts/:id/chats/:chatId/open Open', () => req('POST',`/api/tg-accounts/${accountId}/chats/999/open`).expect(201)); it('POST /tg-accounts/:id/chats/:chatId/close Close', () => req('POST',`/api/tg-accounts/${accountId}/chats/999/close`).expect(201)); }); // ===================================================== // MODULE 8: TG Contacts (5 endpoints) // ===================================================== describe('[8] TG Contacts Module', () => { it('GET /tg-accounts/:id/contacts Get contacts', () => req('GET',`/api/tg-accounts/${accountId}/contacts`).expect(200)); it('GET /tg-accounts/:id/contacts/synced Synced', () => req('GET',`/api/tg-accounts/${accountId}/contacts/synced`).expect(200)); it('POST /tg-accounts/:id/contacts/import Import', () => req('POST',`/api/tg-accounts/${accountId}/contacts/import`, { contacts:[{phoneNumber:'+86'}] }).expect(201)); it('DELETE /tg-accounts/:id/contacts/remove Remove', () => req('DELETE',`/api/tg-accounts/${accountId}/contacts/remove`, { userIds:[1] }).expect(200)); it('GET /tg-accounts/:id/contacts/search Search', () => req('GET',`/api/tg-accounts/${accountId}/contacts/search?q=+86`).expect(200)); }); // ===================================================== // MODULE 9: TG Dialogs (4 endpoints) // ===================================================== describe('[9] TG Dialogs Module', () => { it('GET /tg-accounts/:id/dialogs/me Get me', () => req('GET',`/api/tg-accounts/${accountId}/dialogs/me`).expect(200)); it('POST /tg-accounts/:id/dialogs/open-by-phone By phone', () => req('POST',`/api/tg-accounts/${accountId}/dialogs/open-by-phone`, { phoneNumber:'+86' }).expect(201)); it('POST /tg-accounts/:id/dialogs/open-by-username By username', () => req('POST',`/api/tg-accounts/${accountId}/dialogs/open-by-username`, { username:'test' }).expect(201)); it('POST /tg-accounts/:id/dialogs/open-by-user/:id By user ID', () => req('POST',`/api/tg-accounts/${accountId}/dialogs/open-by-user/456`).expect(201)); }); // ===================================================== // MODULE 10: TG Groups (4 endpoints) // ===================================================== describe('[10] TG Groups Module', () => { it('POST /tg-accounts/:id/groups Create', () => req('POST',`/api/tg-accounts/${accountId}/groups`, { title:'G' }).expect(201)); it('GET /tg-accounts/:id/groups/:chatId Get', () => req('GET',`/api/tg-accounts/${accountId}/groups/111`).expect(200)); it('GET /tg-accounts/:id/groups/:chatId/members Members', () => req('GET',`/api/tg-accounts/${accountId}/groups/111/members`).expect(200)); it('GET /tg-accounts/:id/groups/:chatId/invite-link Invite', () => req('GET',`/api/tg-accounts/${accountId}/groups/111/invite-link`).expect(200)); }); // ===================================================== // MODULE 11: TG Channels (4 endpoints) // ===================================================== describe('[11] TG Channels Module', () => { it('POST /tg-accounts/:id/channels Create', () => req('POST',`/api/tg-accounts/${accountId}/channels`, { title:'C' }).expect(201)); it('GET /tg-accounts/:id/channels/:chatId Get', () => req('GET',`/api/tg-accounts/${accountId}/channels/222`).expect(200)); it('POST /tg-accounts/:id/channels/join Join', () => req('POST',`/api/tg-accounts/${accountId}/channels/join`, { inviteLink:'https://t.me/a' }).expect(201)); it('GET /tg-accounts/:id/channels/:chatId/members Members', () => req('GET',`/api/tg-accounts/${accountId}/channels/222/members`).expect(200)); }); // ===================================================== // MODULE 12: TG Messages (7 endpoints) // ===================================================== describe('[12] TG Messages Module', () => { it('POST /tg-accounts/:id/messages/send Send (queued)', () => req('POST',`/api/tg-accounts/${accountId}/messages/send`, { chatId:123, text:'hi' }).expect(201)); it('POST /tg-accounts/:id/messages/send-direct Send (direct)', () => req('POST',`/api/tg-accounts/${accountId}/messages/send-direct`, { chatId:123, text:'hi' }).expect(201)); it('POST /tg-accounts/:id/messages/edit Edit', () => req('POST',`/api/tg-accounts/${accountId}/messages/edit`, { chatId:123, messageId:456 }).expect(201)); it('POST /tg-accounts/:id/messages/forward Forward', () => req('POST',`/api/tg-accounts/${accountId}/messages/forward`, { fromChatId:1, messageId:2, toChatId:3 }).expect(201)); it('POST /tg-accounts/:id/messages/delete Delete', () => req('POST',`/api/tg-accounts/${accountId}/messages/delete`, { chatId:123, messageIds:[456] }).expect(201)); it('POST /tg-accounts/:id/messages/search Search', () => req('POST',`/api/tg-accounts/${accountId}/messages/search`, { query:'hello' }).expect(201)); it('GET /tg-accounts/:id/messages/jobs/:jid Job status', () => req('GET',`/api/tg-accounts/${accountId}/messages/jobs/job-1`).expect(200)); }); // ===================================================== // MODULE 13: TG Files (4 endpoints) // ===================================================== describe('[13] TG Files Module', () => { it('GET /tg-accounts/:id/files List', () => req('GET',`/api/tg-accounts/${accountId}/files`).expect(200)); it('GET /tg-accounts/:id/files/:fid Get', () => req('GET',`/api/tg-accounts/${accountId}/files/file-1`).expect(200)); it('GET /tg-accounts/:id/files/tdlib/:fid TDLib info', () => req('GET',`/api/tg-accounts/${accountId}/files/tdlib/999`).expect(200)); it('POST /tg-accounts/:id/files/download/:fid Download', () => req('POST',`/api/tg-accounts/${accountId}/files/download/999`).expect(201)); }); // ===================================================== // MODULE 14: TG Webhooks (6 endpoints) // ===================================================== describe('[14] TG Webhooks Module', () => { const whId = 'wh-test-1'; it('POST /tg-accounts/:id/webhooks Create (validates)', async () => { await req('POST',`/api/tg-accounts/${accountId}/webhooks`, {}).expect(400); mockTgWebhooksService.create.mockResolvedValue({ id:whId }); await req('POST',`/api/tg-accounts/${accountId}/webhooks`, { url:'https://example.com/hook', events:['message.received'] }).expect(201); }); it('GET /tg-accounts/:id/webhooks List', () => req('GET',`/api/tg-accounts/${accountId}/webhooks`).expect(200)); it('GET /tg-accounts/:id/webhooks/:id Get', () => req('GET',`/api/tg-accounts/${accountId}/webhooks/${whId}`).expect(200)); it('PUT /tg-accounts/:id/webhooks/:id Update', () => req('PUT',`/api/tg-accounts/${accountId}/webhooks/${whId}`, { isActive:false }).expect(200)); it('GET /tg-accounts/:id/webhooks/:id/deliveries Deliveries', () => req('GET',`/api/tg-accounts/${accountId}/webhooks/${whId}/deliveries`).expect(200)); it('DELETE /tg-accounts/:id/webhooks/:id Delete', () => req('DELETE',`/api/tg-accounts/${accountId}/webhooks/${whId}`).expect(200)); }); // ===================================================== // CROSS-CUTTING: Response format, error handling // ===================================================== describe('[Format] Response Wrapper & Error Handling', () => { it('all responses have { success, data, timestamp }', async () => { const res = await request(app.getHttpServer()).get('/api/admin/health'); expect(res.body).toHaveProperty('success', true); expect(res.body).toHaveProperty('data'); expect(res.body).toHaveProperty('timestamp'); }); it('error responses have { success:false, errorCode, message }', async () => { const res = await request(app.getHttpServer()) .post('/api/auth/register') .send({ email: 'bad' }); expect(res.body.success).toBe(false); expect(res.body).toHaveProperty('errorCode'); expect(res.body).toHaveProperty('message'); }); it('unknown routes return 404', async () => { await request(app.getHttpServer()).get('/api/does-not-exist').expect(404); }); it('x-request-id is set in response headers', async () => { const res = await request(app.getHttpServer()).get('/api/admin/health'); expect(res.headers['x-request-id']).toBeDefined(); }); }); });