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
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:
parent
e16b970a2c
commit
1a5e040ed8
347
src/modules/ai-runtime/user-ai.controller.spec.ts
Normal file
347
src/modules/ai-runtime/user-ai.controller.spec.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -10,7 +10,7 @@ export class UserAiController {
|
|||||||
|
|
||||||
@Get('profile')
|
@Get('profile')
|
||||||
async getProfile(@Req() req: any) {
|
async getProfile(@Req() req: any) {
|
||||||
return this.service.getProfile(req.user.id) ?? {};
|
return (await this.service.getProfile(req.user.id)) ?? {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put('profile')
|
@Put('profile')
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user