import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; import request from 'supertest'; import { AppModule } from '../src/app.module'; describe('M0 E2E Tests', () => { let app: INestApplication; beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); app = moduleFixture.createNestApplication(); app.setGlobalPrefix('api', { exclude: ['admin-api/(.*)', 'internal/(.*)'] }); await app.init(); }); afterAll(async () => { await app.close(); }); // Helper: get admin token by login async function loginAdmin(): Promise { const res = await request(app.getHttpServer()) .post('/admin-api/auth/login') .send({ email: 'admin@zhixi.app', password: 'admin123' }); return res.body?.data?.accessToken || ''; } // ══════════════════════════════════════════════ // M0-01: Common Architecture Foundation // ══════════════════════════════════════════════ describe('M0-01 Common Architecture', () => { it('GET /api → 200 with standard response format', async () => { const res = await request(app.getHttpServer()).get('/api').expect(200); expect(res.body).toHaveProperty('success', true); expect(res.body).toHaveProperty('data'); expect(res.body).toHaveProperty('timestamp'); }); it('POST /api/not-found → 404 with error format', async () => { const res = await request(app.getHttpServer()).post('/api/not-found').expect(404); expect(res.body.success).toBe(false); }); it('x-trace-id header present on every response', async () => { const res = await request(app.getHttpServer()).get('/api'); expect(res.headers).toHaveProperty('x-trace-id'); }); }); // ══════════════════════════════════════════════ // M0-02: Event Bus & Reliability // ══════════════════════════════════════════════ describe('M0-02 Event Bus', () => { let token: string; beforeAll(async () => { token = await loginAdmin(); }); it('GET /admin-api/events → 200 with queue overview', async () => { if (!token) return; const res = await request(app.getHttpServer()) .get('/admin-api/events') .set('Authorization', `Bearer ${token}`) .expect(200); expect(res.body.data).toHaveProperty('queues'); expect(Array.isArray(res.body.data.queues)).toBe(true); expect(res.body.data).toHaveProperty('workers'); }); it('GET /admin-api/events → 401 without token', async () => { await request(app.getHttpServer()).get('/admin-api/events').expect(401); }); it('GET /admin-api/events/:queue/failed → 200', async () => { if (!token) return; await request(app.getHttpServer()) .get('/admin-api/events/ai-analysis/failed') .set('Authorization', `Bearer ${token}`) .expect(200); }); }); // ══════════════════════════════════════════════ // M0-03: Config & Feature Flag // ══════════════════════════════════════════════ describe('M0-03 Config', () => { let token: string; beforeAll(async () => { token = await loginAdmin(); }); it('GET /admin-api/config → 200', async () => { if (!token) return; await request(app.getHttpServer()) .get('/admin-api/config') .set('Authorization', `Bearer ${token}`) .expect(200); }); }); // ══════════════════════════════════════════════ // M0-04: Audit & Security // ══════════════════════════════════════════════ describe('M0-04 Audit', () => { let token: string; beforeAll(async () => { token = await loginAdmin(); }); it('GET /admin-api/audit-logs → 200 with paginated items', async () => { if (!token) return; const res = await request(app.getHttpServer()) .get('/admin-api/audit-logs') .set('Authorization', `Bearer ${token}`) .expect(200); expect(res.body.data).toHaveProperty('items'); expect(res.body.data).toHaveProperty('total'); }); it('GET /admin-api/audit-logs → 401 without token', async () => { await request(app.getHttpServer()).get('/admin-api/audit-logs').expect(401); }); }); // ══════════════════════════════════════════════ // M0-05: Traffic Protection & Resilience // ══════════════════════════════════════════════ describe('M0-05 Traffic', () => { it('POST /admin-api/auth/login → returns known status for invalid login', async () => { const res = await request(app.getHttpServer()) .post('/admin-api/auth/login') .send({ email: 'test@test.com', password: 'wrong' }); expect([400, 401, 429, 403]).toContain(res.status); }); }); // ══════════════════════════════════════════════ // M0-06: Content Safety & Moderation // ══════════════════════════════════════════════ describe('M0-06 Content Safety', () => { it('health endpoint returns safe response', async () => { const res = await request(app.getHttpServer()).get('/api').expect(200); expect(res.body.success).toBe(true); }); }); // ══════════════════════════════════════════════ // M0-07: Observability // ══════════════════════════════════════════════ describe('M0-07 Observability', () => { it('API metrics interceptor records request', async () => { await request(app.getHttpServer()).get('/api').expect(200); // MetricsInterceptor records to ApiMetric table via Prisma mock }); it('x-trace-id is unique per request', async () => { const [r1, r2] = await Promise.all([ request(app.getHttpServer()).get('/api'), request(app.getHttpServer()).get('/api'), ]); const id1 = r1.headers['x-trace-id']; const id2 = r2.headers['x-trace-id']; expect(id1).toBeTruthy(); expect(id2).toBeTruthy(); expect(id1).not.toBe(id2); }); }); // ══════════════════════════════════════════════ // M0-08: AI Gateway // ══════════════════════════════════════════════ describe('M0-08 AI Gateway', () => { let token: string; beforeAll(async () => { token = await loginAdmin(); }); it('GET /admin-api/ai-gateway/status → 200', async () => { if (!token) return; const res = await request(app.getHttpServer()) .get('/admin-api/ai-gateway/status') .set('Authorization', `Bearer ${token}`) .expect(200); expect(res.body.success).toBe(true); }); }); // ══════════════════════════════════════════════ // M0-09: File Storage // ══════════════════════════════════════════════ describe('M0-09 File Storage', () => { it('POST /api/files/upload-url → 401 without token', async () => { await request(app.getHttpServer()) .post('/api/files/upload-url') .send({ fileName: 'test.pdf', mimeType: 'application/pdf', size: 1024 }) .expect(401); }); it('GET /admin-api/files → 200 (admin)', async () => { const token = await loginAdmin(); if (!token) return; const res = await request(app.getHttpServer()) .get('/admin-api/files') .set('Authorization', `Bearer ${token}`) .expect(200); expect(res.body.data).toHaveProperty('items'); expect(res.body.data).toHaveProperty('total'); }); }); // ══════════════════════════════════════════════ // M0-10: Task Queue & Worker // ══════════════════════════════════════════════ describe('M0-10 Task Queue', () => { let token: string; beforeAll(async () => { token = await loginAdmin(); }); it('queue service is registered (module loads)', async () => { const res = await request(app.getHttpServer()).get('/api').expect(200); expect(res.body.success).toBe(true); }); it('GET /admin-api/events → returns all 4 queues', async () => { if (!token) return; const res = await request(app.getHttpServer()) .get('/admin-api/events') .set('Authorization', `Bearer ${token}`) .expect(200); const names = res.body.data.queues.map((q: any) => q.name).sort(); expect(names).toContain('ai-analysis'); expect(names).toContain('document-import'); expect(names).toContain('notification'); expect(names).toContain('domain-events'); }); }); // ══════════════════════════════════════════════ // M0-11: Quota, Billing & Cost // ══════════════════════════════════════════════ describe('M0-11 Quota', () => { let token: string; beforeAll(async () => { token = await loginAdmin(); }); it('GET /admin-api/quota/plans → 200', async () => { if (!token) return; await request(app.getHttpServer()) .get('/admin-api/quota/plans') .set('Authorization', `Bearer ${token}`) .expect(200); }); it('GET /admin-api/quota/costs → 200', async () => { if (!token) return; await request(app.getHttpServer()) .get('/admin-api/quota/costs') .set('Authorization', `Bearer ${token}`) .expect(200); }); }); // ══════════════════════════════════════════════ // M0-12: Secret & Vendor Asset // ══════════════════════════════════════════════ describe('M0-12 Secret', () => { let token: string; beforeAll(async () => { token = await loginAdmin(); }); it('GET /admin-api/secrets → 200', async () => { if (!token) return; await request(app.getHttpServer()) .get('/admin-api/secrets') .set('Authorization', `Bearer ${token}`) .expect(200); }); it('POST /admin-api/secrets → creates encrypted secret', async () => { if (!token) return; const res = await request(app.getHttpServer()) .post('/admin-api/secrets') .set('Authorization', `Bearer ${token}`) .send({ name: `test-e2e-${Date.now()}`, provider: 'deepseek', value: 'sk-test1234567890' }) .expect([200, 201]); if (res.body?.data?.id) { await request(app.getHttpServer()) .delete(`/admin-api/secrets/${res.body.data.id}`) .set('Authorization', `Bearer ${token}`); } }); }); // ══════════════════════════════════════════════ // M0-13: Admin Auth & RBAC // ══════════════════════════════════════════════ describe('M0-13 Admin Auth', () => { it('POST /admin-api/auth/login → 401 with wrong password', async () => { await request(app.getHttpServer()) .post('/admin-api/auth/login') .send({ email: 'admin@zhixi.app', password: 'wrongwrong' }) .expect(401); }); it('POST /admin-api/auth/login → 200 with correct credentials', async () => { const res = await request(app.getHttpServer()) .post('/admin-api/auth/login') .send({ email: 'admin@zhixi.app', password: 'admin123' }); expect(res.status).toBe(200); expect(res.body.success).toBe(true); expect(res.body.data).toHaveProperty('accessToken'); expect(res.body.data).toHaveProperty('adminUser'); }); }); // ══════════════════════════════════════════════ // M0-14: User & Account // ══════════════════════════════════════════════ describe('M0-14 User', () => { it('GET /api/users/me → 401 without token', async () => { await request(app.getHttpServer()).get('/api/users/me').expect(401); }); }); });