'use strict'; /** * Feedback Controller Unit Tests * Tests for feedback event endpoints */ // Mock dependencies before requiring the controller const mockPool = { query: jest.fn(), connect: jest.fn(() => ({ query: jest.fn(), release: jest.fn() })) }; jest.mock('../src/db', () => ({ getPool: jest.fn(() => mockPool) })); jest.mock('uuid', () => ({ v4: jest.fn(() => 'mock-uuid-1234') })); const feedbackController = require('../src/controllers/feedback.controller'); const { getPool } = require('../src/db'); describe('Feedback Controller', () => { let mockReq; let mockRes; let mockPool; beforeEach(() => { mockReq = { body: {}, query: {}, params: {} }; mockRes = { status: jest.fn().mockReturnThis(), json: jest.fn().mockReturnThis() }; mockPool = getPool(); jest.clearAllMocks(); }); describe('createFeedbackEvent', () => { it('should return 400 if event_type is missing', async () => { mockReq.body = { session_id: 'session-1' }; await feedbackController.createFeedbackEvent(mockReq, mockRes); expect(mockRes.status).toHaveBeenCalledWith(400); expect(mockRes.json).toHaveBeenCalledWith({ error: { message: 'event_type is required' } }); }); it('should return 400 for invalid event_type', async () => { mockReq.body = { event_type: 'invalid_type' }; await feedbackController.createFeedbackEvent(mockReq, mockRes); expect(mockRes.status).toHaveBeenCalledWith(400); expect(mockRes.json).toHaveBeenCalledWith( expect.objectContaining({ error: expect.objectContaining({ message: expect.stringContaining('Invalid event_type') }) }) ); }); it('should accept all valid event types', async () => { const validEventTypes = [ 'plan_saved', 'plan_completed', 'rating_positive', 'poi_added', 'plan_abandoned', 'poi_removed', 'rating_negative', 'poi_reordered', 'poi_swapped', 'session_start' ]; for (const eventType of validEventTypes) { mockReq.body = { event_type: eventType }; mockPool.query.mockResolvedValueOnce({}); await feedbackController.createFeedbackEvent(mockReq, mockRes); // Reset for next iteration jest.clearAllMocks(); mockPool = getPool(); } }); it('should create a feedback event successfully', async () => { mockReq.body = { event_type: 'plan_saved', session_id: 'session-123', user_hash: 'user-hash-456', context: { destination: 'Ranchi' }, itinerary_snapshot: [{ poi_id: 'poi-1', name: 'Hundru Falls' }], modifications: [], reward: 1.5 }; mockPool.query.mockResolvedValueOnce({}); await feedbackController.createFeedbackEvent(mockReq, mockRes); expect(mockRes.status).toHaveBeenCalledWith(201); expect(mockRes.json).toHaveBeenCalledWith({ data: expect.objectContaining({ id: 'mock-uuid-1234', event_type: 'plan_saved', session_id: 'session-123', reward: 1.5 }), message: 'Feedback event recorded successfully' }); }); it('should return 503 if database is not available', async () => { mockReq.body = { event_type: 'plan_saved' }; // Override getPool to return null require('../src/db').getPool.mockReturnValueOnce(null); await feedbackController.createFeedbackEvent(mockReq, mockRes); expect(mockRes.status).toHaveBeenCalledWith(503); expect(mockRes.json).toHaveBeenCalledWith({ error: { message: 'Database not available' } }); }); it('should return 500 on database error', async () => { mockReq.body = { event_type: 'plan_saved' }; mockPool.query.mockRejectedValueOnce(new Error('DB connection failed')); await feedbackController.createFeedbackEvent(mockReq, mockRes); expect(mockRes.status).toHaveBeenCalledWith(500); expect(mockRes.json).toHaveBeenCalledWith({ error: { message: 'Failed to record feedback event' } }); }); it('should use default values for optional fields', async () => { mockReq.body = { event_type: 'session_start' }; mockPool.query.mockResolvedValueOnce({}); await feedbackController.createFeedbackEvent(mockReq, mockRes); expect(mockPool.query).toHaveBeenCalledWith( expect.any(String), expect.arrayContaining([ 'mock-uuid-1234', 'session_start', null, // session_id null, // user_hash '{}', // context '[]', // itinerary_snapshot '[]', // modifications 0 // reward ]) ); }); }); describe('listFeedbackEvents', () => { it('should list all feedback events with default pagination', async () => { const mockEvents = [ { id: '1', event_type: 'plan_saved' }, { id: '2', event_type: 'poi_added' } ]; mockPool.query.mockResolvedValueOnce({ rows: mockEvents }); await feedbackController.listFeedbackEvents(mockReq, mockRes); expect(mockRes.json).toHaveBeenCalledWith({ data: mockEvents }); }); it('should filter by event_type', async () => { mockReq.query = { event_type: 'plan_saved' }; mockPool.query.mockResolvedValueOnce({ rows: [] }); await feedbackController.listFeedbackEvents(mockReq, mockRes); expect(mockPool.query).toHaveBeenCalledWith( expect.stringContaining('event_type = $'), expect.arrayContaining(['plan_saved']) ); }); it('should filter by session_id', async () => { mockReq.query = { session_id: 'session-123' }; mockPool.query.mockResolvedValueOnce({ rows: [] }); await feedbackController.listFeedbackEvents(mockReq, mockRes); expect(mockPool.query).toHaveBeenCalledWith( expect.stringContaining('session_id = $'), expect.arrayContaining(['session-123']) ); }); it('should apply custom pagination', async () => { mockReq.query = { limit: '50', offset: '100' }; mockPool.query.mockResolvedValueOnce({ rows: [] }); await feedbackController.listFeedbackEvents(mockReq, mockRes); expect(mockPool.query).toHaveBeenCalledWith( expect.any(String), expect.arrayContaining([50, 100]) ); }); it('should cap limit at 1000', async () => { mockReq.query = { limit: '5000' }; mockPool.query.mockResolvedValueOnce({ rows: [] }); await feedbackController.listFeedbackEvents(mockReq, mockRes); expect(mockPool.query).toHaveBeenCalledWith( expect.any(String), expect.arrayContaining([1000]) ); }); it('should return 503 if database is not available', async () => { require('../src/db').getPool.mockReturnValueOnce(null); await feedbackController.listFeedbackEvents(mockReq, mockRes); expect(mockRes.status).toHaveBeenCalledWith(503); }); it('should return 500 on database error', async () => { mockPool.query.mockRejectedValueOnce(new Error('DB error')); await feedbackController.listFeedbackEvents(mockReq, mockRes); expect(mockRes.status).toHaveBeenCalledWith(500); expect(mockRes.json).toHaveBeenCalledWith({ error: { message: 'Failed to list feedback events' } }); }); }); describe('getPOIStats', () => { it('should return 400 if poi_id is missing', async () => { mockReq.params = {}; await feedbackController.getPOIStats(mockReq, mockRes); expect(mockRes.status).toHaveBeenCalledWith(400); expect(mockRes.json).toHaveBeenCalledWith({ error: { message: 'poi_id is required' } }); }); it('should return aggregated stats for a POI', async () => { mockReq.params = { poi_id: 'poi-123' }; const mockEvents = [ { event_type: 'plan_saved', reward: '1.0', modifications: '[]' }, { event_type: 'plan_saved', reward: '0.5', modifications: '[]' }, { event_type: 'poi_removed', reward: '-0.5', modifications: '[{"poi_id": "poi-123"}]' } ]; mockPool.query.mockResolvedValueOnce({ rows: mockEvents }); await feedbackController.getPOIStats(mockReq, mockRes); expect(mockRes.json).toHaveBeenCalledWith({ data: expect.objectContaining({ poi_id: 'poi-123', times_suggested: 3, times_saved: 2, times_removed: 1 }) }); }); it('should calculate save_rate correctly', async () => { mockReq.params = { poi_id: 'poi-456' }; const mockEvents = [ { event_type: 'plan_saved', reward: '1.0', modifications: '[]' }, { event_type: 'plan_completed', reward: '1.0', modifications: '[]' } ]; mockPool.query.mockResolvedValueOnce({ rows: mockEvents }); await feedbackController.getPOIStats(mockReq, mockRes); expect(mockRes.json).toHaveBeenCalledWith({ data: expect.objectContaining({ times_saved: 1, times_suggested: 2, save_rate: '0.50' }) }); }); it('should return zero stats for POI with no events', async () => { mockReq.params = { poi_id: 'new-poi' }; mockPool.query.mockResolvedValueOnce({ rows: [] }); await feedbackController.getPOIStats(mockReq, mockRes); expect(mockRes.json).toHaveBeenCalledWith({ data: expect.objectContaining({ poi_id: 'new-poi', times_suggested: 0, times_saved: 0, times_removed: 0, avg_reward: 0, save_rate: 0 }) }); }); it('should return 503 if database is not available', async () => { mockReq.params = { poi_id: 'poi-123' }; require('../src/db').getPool.mockReturnValueOnce(null); await feedbackController.getPOIStats(mockReq, mockRes); expect(mockRes.status).toHaveBeenCalledWith(503); }); it('should return 500 on database error', async () => { mockReq.params = { poi_id: 'poi-123' }; mockPool.query.mockRejectedValueOnce(new Error('DB error')); await feedbackController.getPOIStats(mockReq, mockRes); expect(mockRes.status).toHaveBeenCalledWith(500); expect(mockRes.json).toHaveBeenCalledWith({ error: { message: 'Failed to get POI stats' } }); }); }); });