From 294e63a2502c358db50d8def2c915ab097909664 Mon Sep 17 00:00:00 2001 From: malavya1411 Date: Mon, 13 Jul 2026 21:02:59 +0530 Subject: [PATCH] feat: add feedback rate limiting and update testing infrastructure with consistent mocks and config --- backend/jest.config.js | 2 + backend/src/__tests__/calendar-routes.test.ts | 14 +++++++ backend/src/__tests__/digest.test.ts | 16 +++++--- backend/src/__tests__/feedback.test.ts | 34 ++++++++++++++- backend/src/__tests__/health.test.ts | 3 ++ backend/src/__tests__/profile.test.ts | 3 ++ backend/src/__tests__/search.test.ts | 3 ++ backend/src/routes/feedback.routes.ts | 41 ++++++++++++++++--- .../services/ai/feedback-collector.service.ts | 10 +++-- 9 files changed, 110 insertions(+), 16 deletions(-) diff --git a/backend/jest.config.js b/backend/jest.config.js index 347b041..f801d44 100644 --- a/backend/jest.config.js +++ b/backend/jest.config.js @@ -1,4 +1,6 @@ process.env.PORT = '0'; +process.env.DATABASE_URL = 'postgresql://postgres:postgres@localhost:5432/inboxos_test'; +process.env.NODE_ENV = 'test'; module.exports = { preset: 'ts-jest', diff --git a/backend/src/__tests__/calendar-routes.test.ts b/backend/src/__tests__/calendar-routes.test.ts index 92c5ac3..89dee83 100644 --- a/backend/src/__tests__/calendar-routes.test.ts +++ b/backend/src/__tests__/calendar-routes.test.ts @@ -1,4 +1,18 @@ import request from 'supertest'; +jest.mock('../worker', () => ({ + registerWorkerHandlers: () => Promise.resolve(), +})); + +const mockPrisma = { + calendarEvent: { + findMany: jest.fn(), + }, +}; + +jest.mock('@prisma/client', () => ({ + PrismaClient: jest.fn().mockImplementation(() => mockPrisma), +})); + import { app, server, prisma } from '../server'; import { AuthService } from '../services/auth.service'; import { EventBus } from '../services/event-bus.service'; diff --git a/backend/src/__tests__/digest.test.ts b/backend/src/__tests__/digest.test.ts index 322985e..3faa5cb 100644 --- a/backend/src/__tests__/digest.test.ts +++ b/backend/src/__tests__/digest.test.ts @@ -27,11 +27,17 @@ jest.mock('@prisma/client', () => { }); // Mock EmailSenderService -jest.mock('../services/email-sender.service', () => ({ - EmailSenderService: { - send: jest.fn(), - }, -})); +jest.mock('../services/email-sender.service', () => { + class MockGmailAuthError extends Error { + name = 'GmailAuthError'; + } + return { + EmailSenderService: { + send: jest.fn(), + }, + GmailAuthError: MockGmailAuthError, + }; +}); const prisma = new PrismaClient(); diff --git a/backend/src/__tests__/feedback.test.ts b/backend/src/__tests__/feedback.test.ts index eac473c..48718d7 100644 --- a/backend/src/__tests__/feedback.test.ts +++ b/backend/src/__tests__/feedback.test.ts @@ -1,4 +1,27 @@ import request from 'supertest'; +jest.mock('../worker', () => ({ + registerWorkerHandlers: () => Promise.resolve(), +})); + +const mockPrisma = { + email: { + findFirst: jest.fn(), + findUnique: jest.fn(), + }, + userFeedback: { + count: jest.fn(), + create: jest.fn(), + }, + userSettings: { + findUnique: jest.fn(), + update: jest.fn(), + }, +}; + +jest.mock('@prisma/client', () => ({ + PrismaClient: jest.fn().mockImplementation(() => mockPrisma), +})); + import { app, server, prisma } from '../server'; import { AuthService } from '../services/auth.service'; import { EventBus } from '../services/event-bus.service'; @@ -24,6 +47,13 @@ describe('Feedback Collection API and Service', () => { const mockToken = AuthService.generateToken(mockUserId, 'user@example.com'); describe('POST /api/feedback', () => { + beforeEach(() => { + jest.spyOn(prisma.email, 'findFirst').mockResolvedValue({ + id: mockEmailId, + userId: mockUserId, + } as any); + }); + it('should return 401 if unauthorized', async () => { await request(app) .post('/api/feedback') @@ -198,7 +228,7 @@ describe('Feedback Collection API and Service', () => { expect(settingsUpdateSpy).toHaveBeenCalled(); const updatedData = JSON.parse( - settingsUpdateSpy.mock.calls[0][0].data.aiPreferenceProfile as string + (settingsUpdateSpy as any).mock.calls[0][0].data.aiPreferenceProfile as string ); const weekKey = FeedbackCollectorService.getStartOfWeek(); @@ -244,7 +274,7 @@ describe('Feedback Collection API and Service', () => { expect(settingsUpdateSpy).toHaveBeenCalled(); const updatedData = JSON.parse( - settingsUpdateSpy.mock.calls[0][0].data.aiPreferenceProfile as string + (settingsUpdateSpy as any).mock.calls[0][0].data.aiPreferenceProfile as string ); const weekKey = FeedbackCollectorService.getStartOfWeek(); diff --git a/backend/src/__tests__/health.test.ts b/backend/src/__tests__/health.test.ts index 058d617..247ec6c 100644 --- a/backend/src/__tests__/health.test.ts +++ b/backend/src/__tests__/health.test.ts @@ -1,4 +1,7 @@ import request from 'supertest'; +jest.mock('../worker', () => ({ + registerWorkerHandlers: () => Promise.resolve(), +})); import { app, server, prisma } from '../server'; import { EventBus } from '../services/event-bus.service'; diff --git a/backend/src/__tests__/profile.test.ts b/backend/src/__tests__/profile.test.ts index e8dc93f..c2f34ce 100644 --- a/backend/src/__tests__/profile.test.ts +++ b/backend/src/__tests__/profile.test.ts @@ -1,4 +1,7 @@ import request from 'supertest'; +jest.mock('../worker', () => ({ + registerWorkerHandlers: () => Promise.resolve(), +})); import { app, server, prisma } from '../server'; import { AuthService } from '../services/auth.service'; import { EventBus } from '../services/event-bus.service'; diff --git a/backend/src/__tests__/search.test.ts b/backend/src/__tests__/search.test.ts index 03136e8..6c84568 100644 --- a/backend/src/__tests__/search.test.ts +++ b/backend/src/__tests__/search.test.ts @@ -1,4 +1,7 @@ import request from 'supertest'; +jest.mock('../worker', () => ({ + registerWorkerHandlers: () => Promise.resolve(), +})); import { app, server, prisma } from '../server'; import { AuthService } from '../services/auth.service'; import { EventBus } from '../services/event-bus.service'; diff --git a/backend/src/routes/feedback.routes.ts b/backend/src/routes/feedback.routes.ts index 61fb22f..6a18e57 100644 --- a/backend/src/routes/feedback.routes.ts +++ b/backend/src/routes/feedback.routes.ts @@ -6,6 +6,7 @@ import { } from '../middleware/auth.middleware'; import { logger } from '../utils/logger'; import { z } from 'zod'; +import { FeedbackCollectorService } from '../services/ai/feedback-collector.service'; export const feedbackRouter = Router(); const prisma = new PrismaClient(); @@ -41,6 +42,25 @@ feedbackRouter.post( const { feedbackType, feature, emailId, comment } = validation.data; + // Rate limit check: max 100 feedbacks per day per user + const startOfDay = new Date(); + startOfDay.setHours(0, 0, 0, 0); + + const count = await prisma.userFeedback.count({ + where: { + userId, + createdAt: { + gte: startOfDay, + }, + }, + }); + + if (count >= 100) { + return res.status(429).json({ + error: 'Rate limit exceeded: 100 feedbacks per day', + }); + } + // Verify emailId belongs to user if provided if (emailId) { const email = await prisma.email.findFirst({ @@ -51,19 +71,30 @@ feedbackRouter.post( } } - const feedback = await prisma.userFeedback.create({ - data: { userId, feedbackType, feature, emailId, comment }, - }); + let feedbackId = ''; + if (emailId) { + const feedback = await FeedbackCollectorService.recordFeedback( + userId, + emailId, + feedbackType + ); + feedbackId = feedback?.id || ''; + } else { + const feedback = await prisma.userFeedback.create({ + data: { userId, feedbackType, feature, emailId, comment }, + }); + feedbackId = feedback.id; + } logger.info('[Feedback] Recorded', { - id: feedback.id, + id: feedbackId, userId, feedbackType, feature, }); return res .status(201) - .json({ message: 'Feedback recorded', id: feedback.id }); + .json({ message: 'Feedback recorded', id: feedbackId }); } catch (err: any) { logger.error('[Feedback] POST / error:', err.message); return res.status(500).json({ error: 'Failed to record feedback' }); diff --git a/backend/src/services/ai/feedback-collector.service.ts b/backend/src/services/ai/feedback-collector.service.ts index f377847..39b6fee 100644 --- a/backend/src/services/ai/feedback-collector.service.ts +++ b/backend/src/services/ai/feedback-collector.service.ts @@ -32,8 +32,8 @@ export class FeedbackCollectorService { | 'category_correction' | 'priority_adjustment', correctedValue?: string - ): Promise { - // 1. Fetch email by emailId + ): Promise { + // 1. Find email const email = await this.prisma.email.findUnique({ where: { id: emailId }, }); @@ -43,14 +43,14 @@ export class FeedbackCollectorService { console.warn( `[FeedbackCollector] Email not found for feedback (emailId: ${emailId}). Ignoring feedback.` ); - return; + return null; } // 3. Determine original value const originalValue = email.category || 'unclassified'; // 4. Save feedback in the database - await this.prisma.userFeedback.create({ + const feedback = await this.prisma.userFeedback.create({ data: { userId, emailId, @@ -137,5 +137,7 @@ export class FeedbackCollectorService { aiPreferenceProfile: JSON.stringify(profile), }, }); + + return feedback; } }