test: add unit tests for user-ai.controller (API-AI-063)
All checks were successful
Deploy API Server / build-and-deploy (push) Successful in 47s

- 34 tests covering all 28 endpoints
- Profile: GET/PUT with null→{} fallback
- Settings: GET/PUT delegation
- Credentials: CRUD + test (6 endpoints)
- Analysis Jobs: create/cancel/get/list with query filters
- Publish: quiz + flashcard
- Analysis Results: getAnalysis/listAnalyses/listRecommendations/listWeakPoints
- Quizzes: get/getQuestions/list with filters
- Reanalysis/Notifications/Feedback/Flashcards
- Fix: getProfile ?? {} now awaits before null-coalescing

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
wangdl 2026-06-18 11:57:38 +08:00
parent e16b970a2c
commit 1a5e040ed8
2 changed files with 348 additions and 1 deletions

View File

@ -0,0 +1,347 @@
import { UserAiController } from './user-ai.controller';
describe('UserAiController', () => {
let controller: UserAiController;
let mockService: any;
beforeEach(() => {
mockService = {
getProfile: jest.fn(),
saveProfile: jest.fn(),
getSettings: jest.fn(),
updateSettings: jest.fn(),
listCredentials: jest.fn(),
createCredential: jest.fn(),
updateCredential: jest.fn(),
deleteCredential: jest.fn(),
testCredential: jest.fn(),
createAnalysisJob: jest.fn(),
cancelJob: jest.fn(),
getJob: jest.fn(),
listJobs: jest.fn(),
publishQuiz: jest.fn(),
publishFlashcard: jest.fn(),
getAnalysis: jest.fn(),
listAnalyses: jest.fn(),
listRecommendations: jest.fn(),
listWeakPoints: jest.fn(),
getQuiz: jest.fn(),
getQuizQuestions: jest.fn(),
listQuizzes: jest.fn(),
triggerReanalysis: jest.fn(),
listNotifications: jest.fn(),
submitArtifactFeedback: jest.fn(),
submitFeedback: jest.fn(),
getFlashcard: jest.fn(),
listFlashcards: jest.fn(),
};
controller = new UserAiController(mockService);
});
const req = (id = 'u1') => ({ user: { id } }) as any;
// ═══════════════════════════════════════════════════════════════════
// Profile
// ═══════════════════════════════════════════════════════════════════
describe('Profile', () => {
it('GET profile returns profile or empty object', async () => {
mockService.getProfile.mockResolvedValue({ learningGoal: 'exam' });
const result = await controller.getProfile(req());
expect(result).toEqual({ learningGoal: 'exam' });
expect(mockService.getProfile).toHaveBeenCalledWith('u1');
});
it('GET profile returns {} when service returns null', async () => {
mockService.getProfile.mockResolvedValue(null);
const result = await controller.getProfile(req());
expect(result).toEqual({});
});
it('PUT profile delegates to service', async () => {
const dto = { learningGoal: 'exam' };
mockService.saveProfile.mockResolvedValue({ id: 'p1' });
const result = await controller.saveProfile(req(), dto as any);
expect(mockService.saveProfile).toHaveBeenCalledWith('u1', dto);
expect(result).toEqual({ id: 'p1' });
});
});
// ═══════════════════════════════════════════════════════════════════
// Settings
// ═══════════════════════════════════════════════════════════════════
describe('Settings', () => {
it('GET settings', async () => {
mockService.getSettings.mockResolvedValue({ allowAiAnalysis: true });
const result = await controller.getSettings(req());
expect(mockService.getSettings).toHaveBeenCalledWith('u1');
expect(result).toEqual({ allowAiAnalysis: true });
});
it('PUT settings', async () => {
const dto = { allowAiAnalysis: false };
mockService.updateSettings.mockResolvedValue({ id: 's1' });
const result = await controller.updateSettings(req(), dto as any);
expect(mockService.updateSettings).toHaveBeenCalledWith('u1', dto);
expect(result).toEqual({ id: 's1' });
});
});
// ═══════════════════════════════════════════════════════════════════
// Model Credentials
// ═══════════════════════════════════════════════════════════════════
describe('Model Credentials', () => {
it('GET model-credentials lists credentials', async () => {
mockService.listCredentials.mockResolvedValue([{ id: 'c1' }]);
const result = await controller.listCredentials(req());
expect(mockService.listCredentials).toHaveBeenCalledWith('u1');
expect(result).toEqual([{ id: 'c1' }]);
});
it('POST model-credentials creates credential', async () => {
const dto = { name: 'MyKey', apiKey: 'sk-xxx' };
mockService.createCredential.mockResolvedValue({ id: 'c1' });
const result = await controller.createCredential(req(), dto as any);
expect(mockService.createCredential).toHaveBeenCalledWith('u1', dto);
expect(result).toEqual({ id: 'c1' });
});
it('PUT model-credentials/:id updates credential', async () => {
const dto = { name: 'Updated' };
mockService.updateCredential.mockResolvedValue({ id: 'c1' });
const result = await controller.updateCredential(req(), 'c1', dto as any);
expect(mockService.updateCredential).toHaveBeenCalledWith('u1', 'c1', dto);
expect(result).toEqual({ id: 'c1' });
});
it('DELETE model-credentials/:id returns { ok: true }', async () => {
mockService.deleteCredential.mockResolvedValue(undefined);
const result = await controller.deleteCredential(req(), 'c1');
expect(mockService.deleteCredential).toHaveBeenCalledWith('u1', 'c1');
expect(result).toEqual({ ok: true });
});
it('POST model-credentials/:id/test tests credential', async () => {
mockService.testCredential.mockResolvedValue({ success: true });
const result = await controller.testCredential(req(), 'c1');
expect(mockService.testCredential).toHaveBeenCalledWith('u1', 'c1');
expect(result).toEqual({ success: true });
});
});
// ═══════════════════════════════════════════════════════════════════
// Analysis Jobs
// ═══════════════════════════════════════════════════════════════════
describe('Analysis Jobs', () => {
it('POST jobs creates analysis job', async () => {
const dto = { jobType: 'quiz_generation', targetType: 'knowledge_base', targetId: 'kb1' };
mockService.createAnalysisJob.mockResolvedValue({ jobId: 'j1' });
const result = await controller.createAnalysisJob(req(), dto as any);
expect(mockService.createAnalysisJob).toHaveBeenCalledWith('u1', dto);
expect(result).toEqual({ jobId: 'j1' });
});
it('POST jobs/:jobId/cancel cancels job', async () => {
mockService.cancelJob.mockResolvedValue({ status: 'cancelled' });
const result = await controller.cancelJob(req(), 'j1');
expect(mockService.cancelJob).toHaveBeenCalledWith('u1', 'j1');
expect(result).toEqual({ status: 'cancelled' });
});
it('GET jobs/:jobId gets job detail', async () => {
mockService.getJob.mockResolvedValue({ id: 'j1', status: 'running' });
const result = await controller.getJob(req(), 'j1');
expect(mockService.getJob).toHaveBeenCalledWith('u1', 'j1');
expect(result).toEqual({ id: 'j1', status: 'running' });
});
it('GET jobs lists jobs with query filters', async () => {
mockService.listJobs.mockResolvedValue([{ id: 'j1' }]);
const result = await controller.listJobs(req(), 'running', '10');
expect(mockService.listJobs).toHaveBeenCalledWith('u1', 'running', 10);
expect(result).toEqual([{ id: 'j1' }]);
});
it('GET jobs without take returns undefined', async () => {
mockService.listJobs.mockResolvedValue([]);
await controller.listJobs(req(), undefined, undefined);
expect(mockService.listJobs).toHaveBeenCalledWith('u1', undefined, undefined);
});
});
// ═══════════════════════════════════════════════════════════════════
// Quiz & Flashcard Publish
// ═══════════════════════════════════════════════════════════════════
describe('Publish', () => {
it('POST quizzes/:quizId/publish', async () => {
mockService.publishQuiz.mockResolvedValue({ status: 'published' });
const result = await controller.publishQuiz(req(), 'q1');
expect(mockService.publishQuiz).toHaveBeenCalledWith('u1', 'q1');
expect(result).toEqual({ status: 'published' });
});
it('POST flashcards/:cardId/publish', async () => {
mockService.publishFlashcard.mockResolvedValue({ status: 'published' });
const result = await controller.publishFlashcard(req(), 'fc1');
expect(mockService.publishFlashcard).toHaveBeenCalledWith('u1', 'fc1');
expect(result).toEqual({ status: 'published' });
});
});
// ═══════════════════════════════════════════════════════════════════
// Analysis Results
// ═══════════════════════════════════════════════════════════════════
describe('Analysis Results', () => {
it('GET analyses/:id', async () => {
mockService.getAnalysis.mockResolvedValue({ id: 'a1' });
const result = await controller.getAnalysis(req(), 'a1');
expect(mockService.getAnalysis).toHaveBeenCalledWith('u1', 'a1');
expect(result).toEqual({ id: 'a1' });
});
it('GET analyses with filters', async () => {
mockService.listAnalyses.mockResolvedValue([{ id: 'a1' }]);
const result = await controller.listAnalyses(req(), 'material', 'm1', '5');
expect(mockService.listAnalyses).toHaveBeenCalledWith('u1', 'material', 'm1', 5);
expect(result).toEqual([{ id: 'a1' }]);
});
it('GET analyses without filters', async () => {
mockService.listAnalyses.mockResolvedValue([]);
await controller.listAnalyses(req());
expect(mockService.listAnalyses).toHaveBeenCalledWith('u1', undefined, undefined, undefined);
});
it('GET recommendations with filters', async () => {
mockService.listRecommendations.mockResolvedValue([{ id: 'r1' }]);
const result = await controller.listRecommendations(req(), 'material', 'm1', 'active', '5');
expect(mockService.listRecommendations).toHaveBeenCalledWith('u1', 'material', 'm1', 'active', 5);
expect(result).toEqual([{ id: 'r1' }]);
});
it('GET recommendations without filters', async () => {
mockService.listRecommendations.mockResolvedValue([]);
await controller.listRecommendations(req());
expect(mockService.listRecommendations).toHaveBeenCalledWith('u1', undefined, undefined, undefined, undefined);
});
it('GET weak-points with filters', async () => {
mockService.listWeakPoints.mockResolvedValue([{ id: 'w1' }]);
const result = await controller.listWeakPoints(req(), 'material', 'm1', 'active', '5');
expect(mockService.listWeakPoints).toHaveBeenCalledWith('u1', 'material', 'm1', 'active', 5);
expect(result).toEqual([{ id: 'w1' }]);
});
});
// ═══════════════════════════════════════════════════════════════════
// Quizzes
// ═══════════════════════════════════════════════════════════════════
describe('Quizzes', () => {
it('GET quizzes/:quizId', async () => {
mockService.getQuiz.mockResolvedValue({ id: 'q1', title: 'Test' });
const result = await controller.getQuiz(req(), 'q1');
expect(mockService.getQuiz).toHaveBeenCalledWith('u1', 'q1');
expect(result).toEqual({ id: 'q1', title: 'Test' });
});
it('GET quizzes/:quizId/questions', async () => {
mockService.getQuizQuestions.mockResolvedValue([{ id: 'qq1' }]);
const result = await controller.getQuizQuestions(req(), 'q1');
expect(mockService.getQuizQuestions).toHaveBeenCalledWith('u1', 'q1');
expect(result).toEqual([{ id: 'qq1' }]);
});
it('GET quizzes with filters', async () => {
mockService.listQuizzes.mockResolvedValue([{ id: 'q1' }]);
const result = await controller.listQuizzes(req(), 'kb1', 'ready', '10');
expect(mockService.listQuizzes).toHaveBeenCalledWith('u1', 'kb1', 'ready', 10);
expect(result).toEqual([{ id: 'q1' }]);
});
});
// ═══════════════════════════════════════════════════════════════════
// Reanalysis
// ═══════════════════════════════════════════════════════════════════
describe('Reanalysis', () => {
it('POST reanalyze triggers reanalysis', async () => {
mockService.triggerReanalysis.mockResolvedValue({ jobId: 'j1' });
const result = await controller.triggerReanalysis(req(), 'material', 'm1');
expect(mockService.triggerReanalysis).toHaveBeenCalledWith('u1', 'material', 'm1');
expect(result).toEqual({ jobId: 'j1' });
});
});
// ═══════════════════════════════════════════════════════════════════
// Notifications
// ═══════════════════════════════════════════════════════════════════
describe('Notifications', () => {
it('GET notifications with take', async () => {
mockService.listNotifications.mockResolvedValue([{ id: 'n1' }]);
const result = await controller.listNotifications(req(), '20');
expect(mockService.listNotifications).toHaveBeenCalledWith('u1', 20);
expect(result).toEqual([{ id: 'n1' }]);
});
it('GET notifications without take', async () => {
mockService.listNotifications.mockResolvedValue([]);
await controller.listNotifications(req());
expect(mockService.listNotifications).toHaveBeenCalledWith('u1', undefined);
});
});
// ═══════════════════════════════════════════════════════════════════
// Feedback
// ═══════════════════════════════════════════════════════════════════
describe('Feedback', () => {
it('POST artifacts/:type/:id/feedback', async () => {
const dto = { feedbackType: 'like', reason: 'good' };
mockService.submitArtifactFeedback.mockResolvedValue({ ok: true });
const result = await controller.submitArtifactFeedback(req(), 'quiz', 'q1', dto);
expect(mockService.submitArtifactFeedback).toHaveBeenCalledWith('u1', 'quiz', 'q1', dto);
expect(result).toEqual({ ok: true });
});
it('POST feedback', async () => {
const dto = { category: 'bug', content: 'issue desc', email: 'a@b.com' };
mockService.submitFeedback.mockResolvedValue({ ok: true });
const result = await controller.submitFeedback(req(), dto);
expect(mockService.submitFeedback).toHaveBeenCalledWith('u1', dto);
expect(result).toEqual({ ok: true });
});
});
// ═══════════════════════════════════════════════════════════════════
// Flashcards
// ═══════════════════════════════════════════════════════════════════
describe('Flashcards', () => {
it('GET flashcards/:cardId', async () => {
mockService.getFlashcard.mockResolvedValue({ id: 'fc1', front: 'Q', back: 'A' });
const result = await controller.getFlashcard(req(), 'fc1');
expect(mockService.getFlashcard).toHaveBeenCalledWith('u1', 'fc1');
expect(result).toEqual({ id: 'fc1', front: 'Q', back: 'A' });
});
it('GET flashcards with filters', async () => {
mockService.listFlashcards.mockResolvedValue([{ id: 'fc1' }]);
const result = await controller.listFlashcards(req(), 'kp1', 'draft', '10');
expect(mockService.listFlashcards).toHaveBeenCalledWith('u1', 'kp1', 'draft', 10);
expect(result).toEqual([{ id: 'fc1' }]);
});
it('GET flashcards without filters', async () => {
mockService.listFlashcards.mockResolvedValue([]);
await controller.listFlashcards(req());
expect(mockService.listFlashcards).toHaveBeenCalledWith('u1', undefined, undefined, undefined);
});
});
});

View File

@ -10,7 +10,7 @@ export class UserAiController {
@Get('profile')
async getProfile(@Req() req: any) {
return this.service.getProfile(req.user.id) ?? {};
return (await this.service.getProfile(req.user.id)) ?? {};
}
@Put('profile')