const request = require('supertest'); const express = require('express'); const itemRequestsRouter = require('../../../routes/itemRequests'); // Mock all dependencies jest.mock('../../../models', () => ({ ItemRequest: { findAndCountAll: jest.fn(), findAll: jest.fn(), findByPk: jest.fn(), create: jest.fn(), }, ItemRequestResponse: { findByPk: jest.fn(), create: jest.fn(), }, User: jest.fn(), Item: jest.fn(), })); jest.mock('../../../middleware/auth', () => ({ authenticateToken: jest.fn((req, res, next) => { req.user = { id: 1 }; next(); }), })); jest.mock('sequelize', () => ({ Op: { or: Symbol('or'), iLike: Symbol('iLike'), }, })); const { ItemRequest, ItemRequestResponse, User, Item } = require('../../../models'); // Create express app with the router const app = express(); app.use(express.json()); app.use('/item-requests', itemRequestsRouter); // Mock models const mockItemRequestFindAndCountAll = ItemRequest.findAndCountAll; const mockItemRequestFindAll = ItemRequest.findAll; const mockItemRequestFindByPk = ItemRequest.findByPk; const mockItemRequestCreate = ItemRequest.create; const mockItemRequestResponseFindByPk = ItemRequestResponse.findByPk; const mockItemRequestResponseCreate = ItemRequestResponse.create; describe('ItemRequests Routes', () => { beforeEach(() => { jest.clearAllMocks(); }); describe('GET /', () => { it('should get item requests with default pagination and status', async () => { const mockRequestsData = { count: 25, rows: [ { id: 1, title: 'Need a Camera', description: 'Looking for a DSLR camera for weekend photography', status: 'open', requesterId: 2, createdAt: '2024-01-15T10:00:00.000Z', requester: { id: 2, username: 'jane_doe', firstName: 'Jane', lastName: 'Doe' } }, { id: 2, title: 'Power Drill Needed', description: 'Need a drill for home improvement project', status: 'open', requesterId: 3, createdAt: '2024-01-14T10:00:00.000Z', requester: { id: 3, username: 'bob_smith', firstName: 'Bob', lastName: 'Smith' } } ] }; mockItemRequestFindAndCountAll.mockResolvedValue(mockRequestsData); const response = await request(app) .get('/item-requests'); expect(response.status).toBe(200); expect(response.body).toEqual({ requests: mockRequestsData.rows, totalPages: 2, currentPage: 1, totalRequests: 25 }); expect(mockItemRequestFindAndCountAll).toHaveBeenCalledWith({ where: { status: 'open' }, include: [ { model: User, as: 'requester', attributes: ['id', 'username', 'firstName', 'lastName'] } ], limit: 20, offset: 0, order: [['createdAt', 'DESC']] }); }); it('should filter requests with search query', async () => { const mockSearchResults = { count: 5, rows: [ { id: 1, title: 'Need a Camera', description: 'Looking for a DSLR camera', status: 'open' } ] }; mockItemRequestFindAndCountAll.mockResolvedValue(mockSearchResults); const response = await request(app) .get('/item-requests?search=camera&page=1&limit=10'); const { Op } = require('sequelize'); expect(mockItemRequestFindAndCountAll).toHaveBeenCalledWith({ where: { status: 'open', [Op.or]: [ { title: { [Op.iLike]: '%camera%' } }, { description: { [Op.iLike]: '%camera%' } } ] }, include: expect.any(Array), limit: 10, offset: 0, order: [['createdAt', 'DESC']] }); }); it('should handle custom pagination', async () => { const mockData = { count: 50, rows: [] }; mockItemRequestFindAndCountAll.mockResolvedValue(mockData); const response = await request(app) .get('/item-requests?page=3&limit=5'); expect(mockItemRequestFindAndCountAll).toHaveBeenCalledWith({ where: { status: 'open' }, include: expect.any(Array), limit: 5, offset: 10, // (3-1) * 5 order: [['createdAt', 'DESC']] }); }); it('should filter by custom status', async () => { const mockData = { count: 10, rows: [] }; mockItemRequestFindAndCountAll.mockResolvedValue(mockData); await request(app) .get('/item-requests?status=fulfilled'); expect(mockItemRequestFindAndCountAll).toHaveBeenCalledWith({ where: { status: 'fulfilled' }, include: expect.any(Array), limit: 20, offset: 0, order: [['createdAt', 'DESC']] }); }); it('should handle database errors', async () => { mockItemRequestFindAndCountAll.mockRejectedValue(new Error('Database error')); const response = await request(app) .get('/item-requests'); expect(response.status).toBe(500); expect(response.body).toEqual({ error: 'Database error' }); }); }); describe('GET /my-requests', () => { it('should get user\'s own requests with responses', async () => { const mockRequests = [ { id: 1, title: 'My Camera Request', description: 'Need a camera', status: 'open', requesterId: 1, requester: { id: 1, username: 'john_doe', firstName: 'John', lastName: 'Doe' }, responses: [ { id: 1, message: 'I have a Canon DSLR available', responder: { id: 2, username: 'jane_doe', firstName: 'Jane', lastName: 'Doe' }, existingItem: { id: 5, name: 'Canon EOS 5D', description: 'Professional DSLR camera' } } ] } ]; mockItemRequestFindAll.mockResolvedValue(mockRequests); const response = await request(app) .get('/item-requests/my-requests'); expect(response.status).toBe(200); expect(response.body).toEqual(mockRequests); expect(mockItemRequestFindAll).toHaveBeenCalledWith({ where: { requesterId: 1 }, include: [ { model: User, as: 'requester', attributes: ['id', 'username', 'firstName', 'lastName'] }, { model: ItemRequestResponse, as: 'responses', include: [ { model: User, as: 'responder', attributes: ['id', 'username', 'firstName', 'lastName'] }, { model: Item, as: 'existingItem' } ] } ], order: [['createdAt', 'DESC']] }); }); it('should handle database errors', async () => { mockItemRequestFindAll.mockRejectedValue(new Error('Database error')); const response = await request(app) .get('/item-requests/my-requests'); expect(response.status).toBe(500); expect(response.body).toEqual({ error: 'Database error' }); }); }); describe('GET /:id', () => { it('should get specific request with responses', async () => { const mockRequest = { id: 1, title: 'Camera Request', description: 'Need a DSLR camera', status: 'open', requesterId: 2, requester: { id: 2, username: 'jane_doe', firstName: 'Jane', lastName: 'Doe' }, responses: [ { id: 1, message: 'I have a Canon DSLR', responder: { id: 1, username: 'john_doe', firstName: 'John', lastName: 'Doe' }, existingItem: null } ] }; mockItemRequestFindByPk.mockResolvedValue(mockRequest); const response = await request(app) .get('/item-requests/1'); expect(response.status).toBe(200); expect(response.body).toEqual(mockRequest); expect(mockItemRequestFindByPk).toHaveBeenCalledWith('1', { include: [ { model: User, as: 'requester', attributes: ['id', 'username', 'firstName', 'lastName'] }, { model: ItemRequestResponse, as: 'responses', include: [ { model: User, as: 'responder', attributes: ['id', 'username', 'firstName', 'lastName'] }, { model: Item, as: 'existingItem' } ] } ] }); }); it('should return 404 for non-existent request', async () => { mockItemRequestFindByPk.mockResolvedValue(null); const response = await request(app) .get('/item-requests/999'); expect(response.status).toBe(404); expect(response.body).toEqual({ error: 'Item request not found' }); }); it('should handle database errors', async () => { mockItemRequestFindByPk.mockRejectedValue(new Error('Database error')); const response = await request(app) .get('/item-requests/1'); expect(response.status).toBe(500); expect(response.body).toEqual({ error: 'Database error' }); }); }); describe('POST /', () => { it('should create a new item request', async () => { const requestData = { title: 'Need a Drill', description: 'Looking for a power drill for weekend project', category: 'tools', budget: 50, location: 'New York' }; const mockCreatedRequest = { id: 3, ...requestData, requesterId: 1, status: 'open' }; const mockRequestWithRequester = { ...mockCreatedRequest, requester: { id: 1, username: 'john_doe', firstName: 'John', lastName: 'Doe' } }; mockItemRequestCreate.mockResolvedValue(mockCreatedRequest); mockItemRequestFindByPk.mockResolvedValue(mockRequestWithRequester); const response = await request(app) .post('/item-requests') .send(requestData); expect(response.status).toBe(201); expect(response.body).toEqual(mockRequestWithRequester); expect(mockItemRequestCreate).toHaveBeenCalledWith({ ...requestData, requesterId: 1 }); }); it('should handle database errors during creation', async () => { mockItemRequestCreate.mockRejectedValue(new Error('Database error')); const response = await request(app) .post('/item-requests') .send({ title: 'Test Request', description: 'Test description' }); expect(response.status).toBe(500); expect(response.body).toEqual({ error: 'Database error' }); }); }); describe('PUT /:id', () => { const mockRequest = { id: 1, title: 'Original Title', requesterId: 1, update: jest.fn() }; beforeEach(() => { mockItemRequestFindByPk.mockResolvedValue(mockRequest); }); it('should update item request for owner', async () => { const updateData = { title: 'Updated Title', description: 'Updated description' }; const mockUpdatedRequest = { ...mockRequest, ...updateData, requester: { id: 1, username: 'john_doe', firstName: 'John', lastName: 'Doe' } }; mockRequest.update.mockResolvedValue(); mockItemRequestFindByPk .mockResolvedValueOnce(mockRequest) .mockResolvedValueOnce(mockUpdatedRequest); const response = await request(app) .put('/item-requests/1') .send(updateData); expect(response.status).toBe(200); expect(response.body).toEqual({ id: 1, title: 'Updated Title', description: 'Updated description', requesterId: 1, requester: { id: 1, username: 'john_doe', firstName: 'John', lastName: 'Doe' } }); expect(mockRequest.update).toHaveBeenCalledWith(updateData); }); it('should return 404 for non-existent request', async () => { mockItemRequestFindByPk.mockResolvedValue(null); const response = await request(app) .put('/item-requests/999') .send({ title: 'Updated' }); expect(response.status).toBe(404); expect(response.body).toEqual({ error: 'Item request not found' }); }); it('should return 403 for unauthorized user', async () => { const unauthorizedRequest = { ...mockRequest, requesterId: 2 }; mockItemRequestFindByPk.mockResolvedValue(unauthorizedRequest); const response = await request(app) .put('/item-requests/1') .send({ title: 'Updated' }); expect(response.status).toBe(403); expect(response.body).toEqual({ error: 'Unauthorized' }); }); it('should handle database errors', async () => { mockItemRequestFindByPk.mockRejectedValue(new Error('Database error')); const response = await request(app) .put('/item-requests/1') .send({ title: 'Updated' }); expect(response.status).toBe(500); expect(response.body).toEqual({ error: 'Database error' }); }); }); describe('DELETE /:id', () => { const mockRequest = { id: 1, requesterId: 1, destroy: jest.fn() }; beforeEach(() => { mockItemRequestFindByPk.mockResolvedValue(mockRequest); }); it('should delete item request for owner', async () => { mockRequest.destroy.mockResolvedValue(); const response = await request(app) .delete('/item-requests/1'); expect(response.status).toBe(204); expect(mockRequest.destroy).toHaveBeenCalled(); }); it('should return 404 for non-existent request', async () => { mockItemRequestFindByPk.mockResolvedValue(null); const response = await request(app) .delete('/item-requests/999'); expect(response.status).toBe(404); expect(response.body).toEqual({ error: 'Item request not found' }); }); it('should return 403 for unauthorized user', async () => { const unauthorizedRequest = { ...mockRequest, requesterId: 2 }; mockItemRequestFindByPk.mockResolvedValue(unauthorizedRequest); const response = await request(app) .delete('/item-requests/1'); expect(response.status).toBe(403); expect(response.body).toEqual({ error: 'Unauthorized' }); }); it('should handle database errors', async () => { mockItemRequestFindByPk.mockRejectedValue(new Error('Database error')); const response = await request(app) .delete('/item-requests/1'); expect(response.status).toBe(500); expect(response.body).toEqual({ error: 'Database error' }); }); }); describe('POST /:id/responses', () => { const mockRequest = { id: 1, requesterId: 2, status: 'open', increment: jest.fn() }; const mockResponseData = { message: 'I have a drill you can borrow', price: 25, existingItemId: 5 }; const mockCreatedResponse = { id: 1, ...mockResponseData, itemRequestId: 1, responderId: 1 }; const mockResponseWithDetails = { ...mockCreatedResponse, responder: { id: 1, username: 'john_doe', firstName: 'John', lastName: 'Doe' }, existingItem: { id: 5, name: 'Power Drill', description: 'Cordless power drill' } }; beforeEach(() => { mockItemRequestFindByPk.mockResolvedValue(mockRequest); mockItemRequestResponseCreate.mockResolvedValue(mockCreatedResponse); mockItemRequestResponseFindByPk.mockResolvedValue(mockResponseWithDetails); }); it('should create a response to item request', async () => { mockRequest.increment.mockResolvedValue(); const response = await request(app) .post('/item-requests/1/responses') .send(mockResponseData); expect(response.status).toBe(201); expect(response.body).toEqual(mockResponseWithDetails); expect(mockItemRequestResponseCreate).toHaveBeenCalledWith({ ...mockResponseData, itemRequestId: '1', responderId: 1 }); expect(mockRequest.increment).toHaveBeenCalledWith('responseCount'); }); it('should return 404 for non-existent request', async () => { mockItemRequestFindByPk.mockResolvedValue(null); const response = await request(app) .post('/item-requests/999/responses') .send(mockResponseData); expect(response.status).toBe(404); expect(response.body).toEqual({ error: 'Item request not found' }); }); it('should prevent responding to own request', async () => { const ownRequest = { ...mockRequest, requesterId: 1 }; mockItemRequestFindByPk.mockResolvedValue(ownRequest); const response = await request(app) .post('/item-requests/1/responses') .send(mockResponseData); expect(response.status).toBe(400); expect(response.body).toEqual({ error: 'Cannot respond to your own request' }); }); it('should prevent responding to closed request', async () => { const closedRequest = { ...mockRequest, status: 'fulfilled' }; mockItemRequestFindByPk.mockResolvedValue(closedRequest); const response = await request(app) .post('/item-requests/1/responses') .send(mockResponseData); expect(response.status).toBe(400); expect(response.body).toEqual({ error: 'Cannot respond to closed request' }); }); it('should handle database errors', async () => { mockItemRequestResponseCreate.mockRejectedValue(new Error('Database error')); const response = await request(app) .post('/item-requests/1/responses') .send(mockResponseData); expect(response.status).toBe(500); expect(response.body).toEqual({ error: 'Database error' }); }); }); describe('PUT /responses/:responseId/status', () => { const mockResponse = { id: 1, status: 'pending', itemRequest: { id: 1, requesterId: 1 }, update: jest.fn() }; beforeEach(() => { mockItemRequestResponseFindByPk.mockResolvedValue(mockResponse); }); it('should update response status to accepted and fulfill request', async () => { const updatedResponse = { ...mockResponse, status: 'accepted', responder: { id: 2, username: 'jane_doe', firstName: 'Jane', lastName: 'Doe' }, existingItem: null }; mockResponse.update.mockResolvedValue(); mockResponse.itemRequest.update = jest.fn().mockResolvedValue(); mockItemRequestResponseFindByPk .mockResolvedValueOnce(mockResponse) .mockResolvedValueOnce(updatedResponse); const response = await request(app) .put('/item-requests/responses/1/status') .send({ status: 'accepted' }); expect(response.status).toBe(200); expect(response.body).toEqual({ id: 1, status: 'accepted', itemRequest: { id: 1, requesterId: 1 }, responder: { id: 2, username: 'jane_doe', firstName: 'Jane', lastName: 'Doe' }, existingItem: null }); expect(mockResponse.update).toHaveBeenCalledWith({ status: 'accepted' }); expect(mockResponse.itemRequest.update).toHaveBeenCalledWith({ status: 'fulfilled' }); }); it('should update response status without fulfilling request', async () => { const updatedResponse = { ...mockResponse, status: 'declined' }; mockResponse.update.mockResolvedValue(); mockItemRequestResponseFindByPk .mockResolvedValueOnce(mockResponse) .mockResolvedValueOnce(updatedResponse); const response = await request(app) .put('/item-requests/responses/1/status') .send({ status: 'declined' }); expect(response.status).toBe(200); expect(mockResponse.update).toHaveBeenCalledWith({ status: 'declined' }); expect(mockResponse.itemRequest.update).not.toHaveBeenCalled(); }); it('should return 404 for non-existent response', async () => { mockItemRequestResponseFindByPk.mockResolvedValue(null); const response = await request(app) .put('/item-requests/responses/999/status') .send({ status: 'accepted' }); expect(response.status).toBe(404); expect(response.body).toEqual({ error: 'Response not found' }); }); it('should return 403 for unauthorized user', async () => { const unauthorizedResponse = { ...mockResponse, itemRequest: { ...mockResponse.itemRequest, requesterId: 2 } }; mockItemRequestResponseFindByPk.mockResolvedValue(unauthorizedResponse); const response = await request(app) .put('/item-requests/responses/1/status') .send({ status: 'accepted' }); expect(response.status).toBe(403); expect(response.body).toEqual({ error: 'Only the requester can update response status' }); }); it('should handle database errors', async () => { mockItemRequestResponseFindByPk.mockRejectedValue(new Error('Database error')); const response = await request(app) .put('/item-requests/responses/1/status') .send({ status: 'accepted' }); expect(response.status).toBe(500); expect(response.body).toEqual({ error: 'Database error' }); }); }); describe('Edge cases', () => { it('should handle empty search results', async () => { mockItemRequestFindAndCountAll.mockResolvedValue({ count: 0, rows: [] }); const response = await request(app) .get('/item-requests?search=nonexistent'); expect(response.status).toBe(200); expect(response.body.requests).toEqual([]); expect(response.body.totalRequests).toBe(0); }); it('should handle zero page calculation', async () => { mockItemRequestFindAndCountAll.mockResolvedValue({ count: 0, rows: [] }); const response = await request(app) .get('/item-requests'); expect(response.body.totalPages).toBe(0); }); it('should handle request without optional fields', async () => { const minimalRequest = { title: 'Basic Request', description: 'Simple description' }; const mockCreated = { id: 1, ...minimalRequest, requesterId: 1 }; const mockWithRequester = { ...mockCreated, requester: { id: 1, username: 'test' } }; mockItemRequestCreate.mockResolvedValue(mockCreated); mockItemRequestFindByPk.mockResolvedValue(mockWithRequester); const response = await request(app) .post('/item-requests') .send(minimalRequest); expect(response.status).toBe(201); expect(mockItemRequestCreate).toHaveBeenCalledWith({ ...minimalRequest, requesterId: 1 }); }); }); });