import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import request from 'supertest'; import { AppModule } from '../src/app.module'; describe('H0-01 Apple Login Mock Fallback', () => { const OLD_ENV = { ...process.env }; afterAll(() => { process.env = OLD_ENV; }); describe('Dev mode without APPLE_BUNDLE_ID → mock fallback', () => { let app: INestApplication; beforeAll(async () => { process.env.NODE_ENV = 'development'; process.env.JWT_SECRET = 'test-jwt-secret-for-h0-tests'; delete process.env.APPLE_BUNDLE_ID; const m: TestingModule = await Test.createTestingModule({ imports: [AppModule] }).compile(); app = m.createNestApplication(); app.setGlobalPrefix('api', { exclude: ['admin-api/(.*)', 'internal/(.*)'] }); await app.init(); }); afterAll(async () => { await app.close(); }); it('POST /api/auth/apple → 200 with mock fallback (token >= 4 chars)', async () => { const res = await request(app.getHttpServer()) .post('/api/auth/apple') .send({ identityToken: 'test-apple-id-token-valid' }); expect(res.body.success).toBe(true); expect(res.body.data).toHaveProperty('accessToken'); expect(res.body.data).toHaveProperty('refreshToken'); }); it('POST /api/auth/apple short token → 401', async () => { const res = await request(app.getHttpServer()) .post('/api/auth/apple') .send({ identityToken: 'ab' }); expect(res.body.success).toBe(false); expect(res.body.statusCode).toBe(401); }); }); describe('Production mode without APPLE_BUNDLE_ID → reject', () => { let app: INestApplication; beforeAll(async () => { process.env.NODE_ENV = 'production'; process.env.JWT_SECRET = 'prod-test-jwt-secret-for-h0'; process.env.ADMIN_JWT_ACCESS_SECRET = 'prod-test-admin-jwt-secret'; delete process.env.APPLE_BUNDLE_ID; const m: TestingModule = await Test.createTestingModule({ imports: [AppModule] }).compile(); app = m.createNestApplication(); app.setGlobalPrefix('api', { exclude: ['admin-api/(.*)', 'internal/(.*)'] }); await app.init(); }); afterAll(async () => { await app.close(); }); it('POST /api/auth/apple → 401 (production without Apple config)', async () => { const res = await request(app.getHttpServer()) .post('/api/auth/apple') .send({ identityToken: 'valid-looking-apple-identity-token' }); expect(res.body.success).toBe(false); expect(res.body.statusCode).toBe(401); expect(res.body.message).toContain('未配置'); }); }); describe('Production mode with APPLE_BUNDLE_ID → real verification', () => { let app: INestApplication; beforeAll(async () => { process.env.NODE_ENV = 'production'; process.env.JWT_SECRET = 'prod-test-jwt-secret-for-h0'; process.env.ADMIN_JWT_ACCESS_SECRET = 'prod-test-admin-jwt-secret'; process.env.APPLE_BUNDLE_ID = 'com.test.bundle'; const m: TestingModule = await Test.createTestingModule({ imports: [AppModule] }).compile(); app = m.createNestApplication(); app.setGlobalPrefix('api', { exclude: ['admin-api/(.*)', 'internal/(.*)'] }); await app.init(); }); afterAll(async () => { await app.close(); }); it('POST /api/auth/apple → 200 (jose mocked, valid response)', async () => { const res = await request(app.getHttpServer()) .post('/api/auth/apple') .send({ identityToken: 'any-jwt' }); expect(res.body.success).toBe(true); expect(res.body.data).toHaveProperty('accessToken'); }); }); }); describe('H0-02 InternalAuthGuard', () => { const OLD_ENV = { ...process.env }; const INTERNAL_KEY = 'test-internal-api-key-h0'; afterAll(() => { process.env = OLD_ENV; }); describe('Without internal API key → 401', () => { let app: INestApplication; beforeAll(async () => { process.env.NODE_ENV = 'development'; process.env.JWT_SECRET = 'test-jwt-h0-02'; process.env.INTERNAL_API_KEY = INTERNAL_KEY; const m: TestingModule = await Test.createTestingModule({ imports: [AppModule] }).compile(); app = m.createNestApplication(); app.setGlobalPrefix('api', { exclude: ['admin-api/(.*)', 'internal/(.*)'] }); await app.init(); }); afterAll(async () => { await app.close(); }); it('GET /internal/rag/jobs/next without key → 401', async () => { const res = await request(app.getHttpServer()) .get('/internal/rag/jobs/next'); expect(res.body.success).toBe(false); expect(res.body.statusCode).toBe(401); }); it('GET /internal/rag/jobs/:id without key → 401', async () => { const res = await request(app.getHttpServer()) .get('/internal/rag/jobs/test-id'); expect(res.body.success).toBe(false); expect(res.body.statusCode).toBe(401); }); it('POST /internal/rag/chunks without key → 401', async () => { const res = await request(app.getHttpServer()) .post('/internal/rag/chunks') .send({ chunks: [] }); expect(res.body.success).toBe(false); expect(res.body.statusCode).toBe(401); }); }); describe('With valid internal API key → accessible', () => { let app: INestApplication; beforeAll(async () => { process.env.NODE_ENV = 'development'; process.env.JWT_SECRET = 'test-jwt-h0-02'; process.env.INTERNAL_API_KEY = INTERNAL_KEY; const m: TestingModule = await Test.createTestingModule({ imports: [AppModule] }).compile(); app = m.createNestApplication(); app.setGlobalPrefix('api', { exclude: ['admin-api/(.*)', 'internal/(.*)'] }); await app.init(); }); afterAll(async () => { await app.close(); }); it('GET /internal/rag/jobs/next with valid key → 200', async () => { const res = await request(app.getHttpServer()) .get('/internal/rag/jobs/next') .set('X-Internal-API-Key', INTERNAL_KEY); expect(res.body.success).toBe(true); }); it('POST /internal/rag/chunks with valid key → 200', async () => { const res = await request(app.getHttpServer()) .post('/internal/rag/chunks') .set('X-Internal-API-Key', INTERNAL_KEY) .send({ chunks: [] }); expect(res.body.success).toBe(true); }); it('POST /internal/rag/jobs/:id/claim with valid key → 200', async () => { const res = await request(app.getHttpServer()) .post('/internal/rag/jobs/test-id/claim') .set('X-Internal-API-Key', INTERNAL_KEY) .send({ workerId: 'test-worker' }); expect(res.body.success).toBe(true); }); }); }); describe('H0-03 JwtAuthGuard user status check', () => { const OLD_ENV = { ...process.env }; afterAll(() => { process.env = OLD_ENV; }); let app: INestApplication; let jwtService: any; beforeAll(async () => { process.env.NODE_ENV = 'development'; process.env.JWT_SECRET = 'test-jwt-h0-03'; const m: TestingModule = await Test.createTestingModule({ imports: [AppModule] }).compile(); app = m.createNestApplication(); app.setGlobalPrefix('api', { exclude: ['admin-api/(.*)', 'internal/(.*)'] }); await app.init(); jwtService = app.get(JwtService); }); afterAll(async () => { await app.close(); }); it('active user → 200 accessing /api/*', async () => { const token = await jwtService.signAsync({ sub: 'test-user', email: 'test@test.com', role: 'USER' }); const res = await request(app.getHttpServer()) .get('/api/users/me') .set('Authorization', `Bearer ${token}`); expect(res.body.success).toBe(true); }); it('disabled user → 401', async () => { const token = await jwtService.signAsync({ sub: 'disabled-user', email: 'disabled@test.com', role: 'USER' }); const res = await request(app.getHttpServer()) .get('/api/users/me') .set('Authorization', `Bearer ${token}`); expect(res.body.success).toBe(false); expect(res.body.statusCode).toBe(401); expect(res.body.message).toContain('禁用'); }); it('deleted user → 401', async () => { const token = await jwtService.signAsync({ sub: 'deleted-user', email: 'deleted@test.com', role: 'USER' }); const res = await request(app.getHttpServer()) .get('/api/users/me') .set('Authorization', `Bearer ${token}`); expect(res.body.success).toBe(false); expect(res.body.statusCode).toBe(401); expect(res.body.message).toContain('注销'); }); it('non-existent user → 401', async () => { const token = await jwtService.signAsync({ sub: 'ghost-user', email: 'ghost@test.com', role: 'USER' }); const res = await request(app.getHttpServer()) .get('/api/users/me') .set('Authorization', `Bearer ${token}`); expect(res.body.success).toBe(false); expect(res.body.statusCode).toBe(401); }); it('admin token (type=admin) rejected on /api/*', async () => { const token = await jwtService.signAsync({ sub: 'test-user', type: 'admin', role: 'SUPER_ADMIN' }); const res = await request(app.getHttpServer()) .get('/api/users/me') .set('Authorization', `Bearer ${token}`); expect(res.body.success).toBe(false); expect(res.body.statusCode).toBe(401); expect(res.body.message).toContain('无效'); }); }); describe('H0-04 Refresh token user status check', () => { const OLD_ENV = { ...process.env }; afterAll(() => { process.env = OLD_ENV; }); let app: INestApplication; beforeAll(async () => { process.env.NODE_ENV = 'development'; process.env.JWT_SECRET = 'test-jwt-h0-04'; delete process.env.APPLE_BUNDLE_ID; const m: TestingModule = await Test.createTestingModule({ imports: [AppModule] }).compile(); app = m.createNestApplication(); app.setGlobalPrefix('api', { exclude: ['admin-api/(.*)', 'internal/(.*)'] }); await app.init(); }); afterAll(async () => { await app.close(); }); it('Apple login → get refreshToken → refresh succeeds (active user)', async () => { // Login to get a refresh token const loginRes = await request(app.getHttpServer()) .post('/api/auth/apple') .send({ identityToken: 'test-token-for-refresh' }); expect(loginRes.body.success).toBe(true); const { refreshToken } = loginRes.body.data; expect(refreshToken).toBeTruthy(); // Refresh with the token const refreshRes = await request(app.getHttpServer()) .post('/api/auth/refresh') .send({ refreshToken }); expect(refreshRes.body.success).toBe(true); expect(refreshRes.body.data).toHaveProperty('accessToken'); expect(refreshRes.body.data).toHaveProperty('refreshToken'); }); it('POST /api/auth/refresh with invalid token → 401', async () => { const res = await request(app.getHttpServer()) .post('/api/auth/refresh') .send({ refreshToken: 'invalid-or-expired-token' }); expect(res.body.success).toBe(false); expect(res.body.statusCode).toBe(401); }); }); describe('H0-06 CAPI DTO validation', () => { const OLD_ENV = { ...process.env }; afterAll(() => { process.env = OLD_ENV; }); const INTERNAL_KEY = 'test-internal-key-dto'; let app: INestApplication; let jwtService: JwtService; beforeAll(async () => { process.env.NODE_ENV = 'development'; process.env.JWT_SECRET = 'test-jwt-h0-06'; process.env.INTERNAL_API_KEY = INTERNAL_KEY; delete process.env.APPLE_BUNDLE_ID; const m: TestingModule = await Test.createTestingModule({ imports: [AppModule] }).compile(); app = m.createNestApplication(); app.setGlobalPrefix('api', { exclude: ['admin-api/(.*)', 'internal/(.*)'] }); await app.init(); jwtService = app.get(JwtService); }); afterAll(async () => { await app.close(); }); async function getUserToken(): Promise { return jwtService.signAsync({ sub: 'test-user', email: 'test@test.com', role: 'USER', type: 'user' }); } describe('POST /api/imports', () => { it('valid DTO → 201', async () => { const token = await getUserToken(); const res = await request(app.getHttpServer()) .post('/api/imports') .set('Authorization', `Bearer ${token}`) .send({ fileName: 'test.pdf', sourceType: 'file' }); expect(res.body.success).toBe(true); expect(res.body.data).toHaveProperty('jobId'); expect(res.body.data).toHaveProperty('status', 'queued'); }); }); describe('POST /api/knowledge-bases/:kbId/sources', () => { it('valid DTO → 201', async () => { const token = await getUserToken(); const res = await request(app.getHttpServer()) .post('/api/knowledge-bases/test-kb/sources') .set('Authorization', `Bearer ${token}`) .send({ title: '我的笔记', type: 'file', originalFilename: 'notes.pdf' }); expect(res.body.success).toBe(true); expect(res.body.data).toHaveProperty('id'); }); it('invalid type value → 400', async () => { const token = await getUserToken(); const res = await request(app.getHttpServer()) .post('/api/knowledge-bases/test-kb/sources') .set('Authorization', `Bearer ${token}`) .send({ type: 'invalid_type_value' }); expect(res.body.success).toBe(false); expect(res.body.statusCode).toBe(400); }); }); describe('POST /api/learning-sessions', () => { it('valid DTO → 201', async () => { const token = await getUserToken(); const res = await request(app.getHttpServer()) .post('/api/learning-sessions') .set('Authorization', `Bearer ${token}`) .send({ mode: 'active_recall', knowledgeBaseId: 'kb-1' }); expect(res.body.success).toBe(true); }); it('invalid mode → 400', async () => { const token = await getUserToken(); const res = await request(app.getHttpServer()) .post('/api/learning-sessions') .set('Authorization', `Bearer ${token}`) .send({ mode: 'invalid_mode' }); expect(res.body.success).toBe(false); expect(res.body.statusCode).toBe(400); }); }); });