-
Notifications
You must be signed in to change notification settings - Fork 0
Refactor: Improve modularity and readability of backend services. #69
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,103 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import express, { type Application, type Request, type Response, type NextFunction } from 'express'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import activityRoutes from './activityRoutes'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { storage } from './storage'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| jest.mock('./storage', () => ({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| storage: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| getRecentActivities: jest.fn(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| createActivity: jest.fn(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| })); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const createApp = (): Application => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const app = express(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| app.use(express.json()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| app.use('/api/activities', activityRoutes); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return app; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| describe('Activity Routes', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let app: Application; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let mockRequest: Partial<Request>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let mockResponse: Partial<Response>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let nextFunction: NextFunction = jest.fn(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| beforeEach(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| app = createApp(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mockRequest = {}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mockResponse = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| json: jest.fn().mockReturnThis(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| status: jest.fn().mockReturnThis(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (storage.getRecentActivities as jest.Mock).mockClear(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (storage.createActivity as jest.Mock).mockClear(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| describe('GET /api/activities', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('should get recent activities and return 200', async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const mockActivities = [{ id: 1, description: 'Test Activity' }]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (storage.getRecentActivities as jest.Mock).mockResolvedValue(mockActivities); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mockRequest = { query: { limit: '5' }, method: 'GET' }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await activityRoutes(mockRequest as Request, mockResponse as Response, nextFunction); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(storage.getRecentActivities).toHaveBeenCalledWith(5); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mockResponse.json).toHaveBeenCalledWith(mockActivities); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('should use default limit if not provided', async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (storage.getRecentActivities as jest.Mock).mockResolvedValue([]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mockRequest = { query: {}, method: 'GET' }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await activityRoutes(mockRequest as Request, mockResponse as Response, nextFunction); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(storage.getRecentActivities).toHaveBeenCalledWith(undefined); // or specific default if defined in handler | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('should return 500 if fetching activities fails', async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (storage.getRecentActivities as jest.Mock).mockRejectedValue(new Error('DB Error')); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mockRequest = { query: {}, method: 'GET' }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await activityRoutes(mockRequest as Request, mockResponse as Response, nextFunction); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mockResponse.status).toHaveBeenCalledWith(500); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mockResponse.json).toHaveBeenCalledWith({ message: 'Failed to fetch activities' }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| describe('POST /api/activities', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('should create an activity and return 201', async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const newActivityData = { type: 'test', description: 'New Activity', icon: 'test', iconBg: 'test', timestamp: 'test' }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const createdActivity = { id: 2, ...newActivityData }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (storage.createActivity as jest.Mock).mockResolvedValue(createdActivity); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mockRequest = { body: newActivityData, method: 'POST' }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await activityRoutes(mockRequest as Request, mockResponse as Response, nextFunction); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(storage.createActivity).toHaveBeenCalledWith(newActivityData); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mockResponse.status).toHaveBeenCalledWith(201); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mockResponse.json).toHaveBeenCalledWith(createdActivity); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('should return 400 for invalid activity data (simulated ZodError)', async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // This test path is challenging to hit directly without supertest | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // if Zod parsing happens inside the route handler before the mock. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const invalidActivityData = { type: '' }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const zodError = new Error("Invalid activity data") as any; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| zodError.errors = [{ message: "Type cannot be empty" }]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| zodError.name = "ZodError"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // If storage.createActivity was called and it threw an error that was then | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // identified as a ZodError equivalent by the route handler: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (storage.createActivity as jest.Mock).mockImplementation(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw zodError; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mockRequest = { body: invalidActivityData, method: 'POST' }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await activityRoutes(mockRequest as Request, mockResponse as Response, nextFunction); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mockResponse.status).toHaveBeenCalledWith(400); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mockResponse.json).toHaveBeenCalledWith(expect.objectContaining({ message: "Invalid activity data" })); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+81
to
+100
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Zod error path is unreachable with current mocking strategy
- (storage.createActivity as jest.Mock).mockImplementation(() => {
- throw zodError;
- });
+ jest.spyOn(insertActivitySchema, 'parse').mockImplementation(() => { throw zodError; });📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [end of server/activityRoutes.test.ts] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,39 @@ | ||||||||||||||||||||||||||||||||||||||||||||||
| import express from "express"; | ||||||||||||||||||||||||||||||||||||||||||||||
| import { storage } from "./storage"; | ||||||||||||||||||||||||||||||||||||||||||||||
| import { insertActivitySchema } from "@shared/schema"; | ||||||||||||||||||||||||||||||||||||||||||||||
| import { z } from "zod"; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const router = express.Router(); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Activities | ||||||||||||||||||||||||||||||||||||||||||||||
| router.get("/", async (req, res) => { | ||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||
| const { limit } = req.query; | ||||||||||||||||||||||||||||||||||||||||||||||
| console.time("storage.getRecentActivities"); | ||||||||||||||||||||||||||||||||||||||||||||||
| const activities = await storage.getRecentActivities( | ||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue (bug_risk): Handle invalid Validate or default |
||||||||||||||||||||||||||||||||||||||||||||||
| limit ? parseInt(limit as string) : undefined | ||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||
| console.timeEnd("storage.getRecentActivities"); | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+11
to
+16
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Validate and parse
-const activities = await storage.getRecentActivities(
- limit ? parseInt(limit as string) : undefined
-);
+const parsedLimit =
+ typeof limit === "string" && /^\d+$/.test(limit) ? parseInt(limit, 10) : undefined;
+
+if (limit && parsedLimit === undefined) {
+ return res.status(400).json({ message: "Query param 'limit' must be a positive integer" });
+}
+
+const activities = await storage.getRecentActivities(parsedLimit);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||
| res.json(activities); | ||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||
| res.status(500).json({ message: "Failed to fetch activities" }); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| router.post("/", async (req, res) => { | ||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||
| const activityData = insertActivitySchema.parse(req.body); | ||||||||||||||||||||||||||||||||||||||||||||||
| console.time("storage.createActivity"); | ||||||||||||||||||||||||||||||||||||||||||||||
| const activity = await storage.createActivity(activityData); | ||||||||||||||||||||||||||||||||||||||||||||||
| console.timeEnd("storage.createActivity"); | ||||||||||||||||||||||||||||||||||||||||||||||
| res.status(201).json(activity); | ||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||
| if (error instanceof z.ZodError) { | ||||||||||||||||||||||||||||||||||||||||||||||
| res.status(400).json({ message: "Invalid activity data", errors: error.errors }); | ||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||
| res.status(500).json({ message: "Failed to create activity" }); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| export default router; | ||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,217 @@ | ||
| import express, { type Application, type Request, type Response, type NextFunction } from 'express'; | ||
| import aiRoutes from './aiRoutes'; | ||
| import { storage } from './storage'; | ||
| import { pythonNLP } from './python-bridge'; | ||
| import { type MappedNLPResult } from './python-bridge'; | ||
|
|
||
| // Mock dependencies | ||
| jest.mock('./storage', () => ({ | ||
| storage: { | ||
| getEmailById: jest.fn(), | ||
| updateEmail: jest.fn(), | ||
| createActivity: jest.fn(), | ||
| getAllCategories: jest.fn(), // Though this might not be used anymore post-refactor | ||
| getCategoryById: jest.fn(), | ||
| }, | ||
| })); | ||
|
|
||
| jest.mock('./python-bridge', () => ({ | ||
| pythonNLP: { | ||
| analyzeEmail: jest.fn(), | ||
| testConnection: jest.fn(), | ||
| }, | ||
| })); | ||
|
|
||
| const createApp = (): Application => { | ||
| const app = express(); | ||
| app.use(express.json()); | ||
| app.use('/api/ai', aiRoutes); | ||
| return app; | ||
| }; | ||
|
|
||
| describe('AI Routes', () => { | ||
| let app: Application; | ||
| let mockRequest: Partial<Request>; | ||
| let mockResponse: Partial<Response>; | ||
| let nextFunction: NextFunction = jest.fn(); | ||
|
|
||
| const mockEmail = { id: 1, subject: 'Test Email', content: 'Test content' }; | ||
| const mockNLPAnalysisResult: MappedNLPResult = { | ||
| topic: 'Work', | ||
| sentiment: 'neutral', | ||
| intent: 'informational', | ||
| urgency: 'low', | ||
| confidence: 0.9, | ||
| categories: ['Work & Business'], // Raw categories from NLP | ||
| keywords: ['test', 'email'], | ||
| reasoning: 'Analyzed by mock', | ||
| suggestedLabels: ['TestLabel'], | ||
| riskFlags: [], | ||
| categoryId: 5, // Assume Python now returns a categoryId | ||
| validation: { | ||
| validationMethod: 'confidence_threshold', | ||
| score: 0.9, | ||
| reliable: true, | ||
| feedback: 'Mock validation', | ||
| }, | ||
| }; | ||
|
|
||
| beforeEach(() => { | ||
| app = createApp(); | ||
| mockRequest = {}; | ||
| mockResponse = { | ||
| json: jest.fn().mockReturnThis(), | ||
| status: jest.fn().mockReturnThis(), | ||
| }; | ||
| // Clear mocks | ||
| (storage.getEmailById as jest.Mock).mockClear(); | ||
| (storage.updateEmail as jest.Mock).mockClear(); | ||
| (storage.createActivity as jest.Mock).mockClear(); | ||
| (storage.getAllCategories as jest.Mock).mockClear(); | ||
| (storage.getCategoryById as jest.Mock).mockClear(); | ||
| (pythonNLP.analyzeEmail as jest.Mock).mockClear(); | ||
| (pythonNLP.testConnection as jest.Mock).mockClear(); | ||
| }); | ||
|
|
||
| describe('POST /api/ai/analyze', () => { | ||
| it('should analyze email and return analysis', async () => { | ||
| (pythonNLP.analyzeEmail as jest.Mock).mockResolvedValue(mockNLPAnalysisResult); | ||
| mockRequest = { body: { subject: 'Test', content: 'Content' } , method: 'POST'}; | ||
|
|
||
| await aiRoutes(mockRequest as Request, mockResponse as Response, nextFunction); | ||
|
|
||
| expect(pythonNLP.analyzeEmail).toHaveBeenCalledWith('Test', 'Content'); | ||
| expect(mockResponse.json).toHaveBeenCalledWith(mockNLPAnalysisResult); | ||
| }); | ||
|
|
||
| it('should return 400 if subject or content is missing', async () => { | ||
| mockRequest = { body: { subject: 'Test' }, method: 'POST' }; // Missing content | ||
|
|
||
| await aiRoutes(mockRequest as Request, mockResponse as Response, nextFunction); | ||
|
|
||
| expect(mockResponse.status).toHaveBeenCalledWith(400); | ||
| expect(mockResponse.json).toHaveBeenCalledWith({ message: 'Subject and content are required' }); | ||
| }); | ||
| }); | ||
|
|
||
| describe('POST /api/ai/categorize', () => { | ||
| it('should auto-categorize an email using categoryId from AI and return success', async () => { | ||
| (storage.getEmailById as jest.Mock).mockResolvedValue(mockEmail); | ||
| (pythonNLP.analyzeEmail as jest.Mock).mockResolvedValue(mockNLPAnalysisResult); // which includes categoryId: 5 | ||
| const updatedEmail = { ...mockEmail, categoryId: 5, confidence: 90 }; | ||
| (storage.updateEmail as jest.Mock).mockResolvedValue(updatedEmail); | ||
| (storage.getCategoryById as jest.Mock).mockResolvedValue({ id: 5, name: 'Work', description: '', color: '', count: 1 }); | ||
|
|
||
|
|
||
| mockRequest = { body: { emailId: 1, autoAnalyze: true }, method: 'POST' }; | ||
| await aiRoutes(mockRequest as Request, mockResponse as Response, nextFunction); | ||
|
|
||
| expect(storage.getEmailById).toHaveBeenCalledWith(1); | ||
| expect(pythonNLP.analyzeEmail).toHaveBeenCalledWith(mockEmail.subject, mockEmail.content); | ||
| expect(storage.updateEmail).toHaveBeenCalledWith(1, { | ||
| categoryId: mockNLPAnalysisResult.categoryId, | ||
| confidence: Math.round(mockNLPAnalysisResult.confidence * 100), | ||
| labels: mockNLPAnalysisResult.suggestedLabels, | ||
| }); | ||
| expect(storage.createActivity).toHaveBeenCalled(); | ||
| expect(mockResponse.json).toHaveBeenCalledWith(expect.objectContaining({ | ||
| success: true, | ||
| email: updatedEmail, | ||
| categoryAssigned: 'Work' | ||
| })); | ||
| }); | ||
|
|
||
| it('should return success:false if AI does not provide categoryId', async () => { | ||
| (storage.getEmailById as jest.Mock).mockResolvedValue(mockEmail); | ||
| const nlpResultWithoutCategoryId = { ...mockNLPAnalysisResult, categoryId: undefined }; | ||
| (pythonNLP.analyzeEmail as jest.Mock).mockResolvedValue(nlpResultWithoutCategoryId); | ||
|
|
||
| mockRequest = { body: { emailId: 1, autoAnalyze: true }, method: 'POST' }; | ||
| await aiRoutes(mockRequest as Request, mockResponse as Response, nextFunction); | ||
|
|
||
| expect(mockResponse.json).toHaveBeenCalledWith(expect.objectContaining({ | ||
| success: false, | ||
| message: "No matching category found or AI analysis did not provide a category ID." | ||
| })); | ||
| }); | ||
|
|
||
| it('should handle manual categorization', async () => { | ||
| (storage.getEmailById as jest.Mock).mockResolvedValue(mockEmail); | ||
| const manualCategoryId = 2; | ||
| const updatedEmail = { ...mockEmail, categoryId: manualCategoryId, confidence: 95 }; | ||
| (storage.updateEmail as jest.Mock).mockResolvedValue(updatedEmail); | ||
|
|
||
| mockRequest = { body: { emailId: 1, autoAnalyze: false, categoryId: manualCategoryId, confidence: 95 }, method: 'POST' }; | ||
| await aiRoutes(mockRequest as Request, mockResponse as Response, nextFunction); | ||
|
|
||
| expect(storage.updateEmail).toHaveBeenCalledWith(1, { categoryId: manualCategoryId, confidence: 95 }); | ||
| expect(storage.createActivity).toHaveBeenCalled(); | ||
| expect(mockResponse.json).toHaveBeenCalledWith({ success: true, email: updatedEmail }); | ||
| }); | ||
| }); | ||
|
|
||
| describe('POST /api/ai/batch-analyze', () => { | ||
| it('should batch analyze emails and use categoryId from AI', async () => { | ||
| const emailIds = [1, 2]; | ||
| const email1 = { id: 1, subject: 'S1', content: 'C1' }; | ||
| const email2 = { id: 2, subject: 'S2', content: 'C2' }; | ||
| const nlpResult1: MappedNLPResult = { ...mockNLPAnalysisResult, categoryId: 5, categories: ["Work"] }; | ||
| const nlpResult2: MappedNLPResult = { ...mockNLPAnalysisResult, categoryId: undefined, categories: ["Personal"] }; // No categoryId for second | ||
|
|
||
| (storage.getEmailById as jest.Mock).mockImplementation(id => { | ||
| if (id === 1) return Promise.resolve(email1); | ||
| if (id === 2) return Promise.resolve(email2); | ||
| return Promise.resolve(undefined); | ||
| }); | ||
| (pythonNLP.analyzeEmail as jest.Mock).mockImplementation((subject) => { | ||
| if (subject === 'S1') return Promise.resolve(nlpResult1); | ||
| if (subject === 'S2') return Promise.resolve(nlpResult2); | ||
| return Promise.resolve(mockNLPAnalysisResult); | ||
| }); | ||
| (storage.updateEmail as jest.Mock).mockResolvedValue(email1); // Simulate update success | ||
| (storage.getCategoryById as jest.Mock).mockResolvedValue({ id: 5, name: 'Work', description: '', color: '', count: 1 }); | ||
|
|
||
|
|
||
| mockRequest = { body: { emailIds }, method: 'POST' }; | ||
| await aiRoutes(mockRequest as Request, mockResponse as Response, nextFunction); | ||
|
|
||
| expect(pythonNLP.analyzeEmail).toHaveBeenCalledTimes(2); | ||
| expect(storage.updateEmail).toHaveBeenCalledTimes(1); // Only for email1 with categoryId | ||
| expect(storage.updateEmail).toHaveBeenCalledWith(1, expect.objectContaining({ categoryId: 5 })); | ||
| expect(storage.createActivity).toHaveBeenCalled(); | ||
| expect(mockResponse.json).toHaveBeenCalledWith(expect.objectContaining({ | ||
| success: true, | ||
| results: expect.arrayContaining([ | ||
| expect.objectContaining({ emailId: 1, success: true, category: 'Work' }), | ||
| expect.objectContaining({ emailId: 2, success: false, reason: 'no_matching_category_id_from_ai' }) | ||
| ]) | ||
| })); | ||
| }); | ||
| }); | ||
|
|
||
| describe('POST /api/ai/validate', () => { | ||
| it('should validate AI analysis and return success', async () => { | ||
| (storage.getEmailById as jest.Mock).mockResolvedValue(mockEmail); | ||
| (pythonNLP.analyzeEmail as jest.Mock).mockResolvedValue(mockNLPAnalysisResult); | ||
|
|
||
| mockRequest = { body: { emailId: 1, userFeedback: 'correct' }, method: 'POST' }; | ||
| await aiRoutes(mockRequest as Request, mockResponse as Response, nextFunction); | ||
|
|
||
| expect(storage.createActivity).toHaveBeenCalled(); | ||
| expect(mockResponse.json).toHaveBeenCalledWith(expect.objectContaining({ success: true })); | ||
| }); | ||
| }); | ||
|
|
||
| describe('GET /api/ai/health', () => { | ||
| it('should return health status from pythonNLP.testConnection', async () => { | ||
| (pythonNLP.testConnection as jest.Mock).mockResolvedValue(true); | ||
| mockRequest = { method: 'GET' }; | ||
|
|
||
| await aiRoutes(mockRequest as Request, mockResponse as Response, nextFunction); | ||
|
|
||
| expect(pythonNLP.testConnection).toHaveBeenCalled(); | ||
| expect(mockResponse.json).toHaveBeenCalledWith(expect.objectContaining({ status: 'healthy' })); | ||
| }); | ||
| }); | ||
| }); | ||
| [end of server/aiRoutes.test.ts] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Router invoked directly – Express middleware chain is bypassed
Calling
await activityRoutes(mockReq, mockRes, next)exercises none of Express’s routing logic (method/path matching, async error propagation, etc.). Usesupertest(or at least issue the request against theappinstance) so the test environment mirrors production behaviour.🤖 Prompt for AI Agents