From 7483a61c5b9f2aabf04d3414e985c86552a6fd40 Mon Sep 17 00:00:00 2001 From: awlfccamp Date: Sun, 8 Jun 2025 21:13:31 -0500 Subject: [PATCH 1/4] add test for events routers --- backend/routers/events.router.test.js | 339 ++++++++++++++++++++++++++ 1 file changed, 339 insertions(+) create mode 100644 backend/routers/events.router.test.js diff --git a/backend/routers/events.router.test.js b/backend/routers/events.router.test.js new file mode 100644 index 000000000..4cf17d9ff --- /dev/null +++ b/backend/routers/events.router.test.js @@ -0,0 +1,339 @@ +const express = require('express'); + +const supertest = require('supertest'); + +// Mock the Event model (only used by the non-refactored route directly in the router) + +// Path corrected to go up one directory (backend/) then down into models/ + +jest.mock('../models/event.model', () => ({ + Event: { + find: jest.fn(), + + // populate, then, and catch will be defined directly on the mock returned by find() + + // within the test cases for the direct router route to handle chaining. + + // For controller-driven routes, the controller itself is mocked. + }, +})); + +const { Event } = require('../models/event.model'); // Path corrected + +// Mock the EventController + +// Path corrected to go up one directory (backend/) then down into controllers/ + +jest.mock('../controllers', () => ({ + EventController: { + event_list: jest.fn(), + + create: jest.fn(), + + event_by_id: jest.fn(), + + destroy: jest.fn(), + + update: jest.fn(), + }, +})); + +const { EventController } = require('../controllers'); // Path corrected + +// Must import eventsRouter after setting up mocks for EventController + +// Path is relative to the current directory (routers/) + +const eventsRouter = require('./events.router'); + +// Setup testapp with just eventsRouter which calls mocked EventController + +const testapp = express(); + +testapp.use(express.json()); + +testapp.use(express.urlencoded({ extended: false })); + +testapp.use('/api/events', eventsRouter); + +const request = supertest(testapp); + +describe('Unit Tests for events.router.js', () => { + // Mock event data for consistent testing + + const mockEvent = { + _id: 'event123', + + name: 'Test Event', + + project: 'projectABC', + + date: '2025-01-01T10:00:00Z', + }; + + const mockEventId = 'event123'; + + const mockProjectId = 'projectABC'; + + const mockUpdatedEventData = { + name: 'Updated Test Event Name', + }; + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('GET /api/events (event_list)', () => { + it('should call EventController.event_list and return a list of events', async (done) => { + // Setup + + EventController.event_list.mockImplementationOnce((req, res) => + res.status(200).send([mockEvent]), + ); + + // Functionality + + const response = await request.get('/api/events'); + + // Test + + expect(EventController.event_list).toHaveBeenCalledWith( + expect.anything(), // req + + expect.anything(), // res + + expect.anything(), // next + ); + + expect(response.status).toBe(200); + + expect(response.body).toEqual([mockEvent]); + + done(); + }); + }); + + describe('POST /api/events (create)', () => { + it('should call EventController.create and return the created event', async (done) => { + // Setup + + EventController.create.mockImplementationOnce((req, res) => res.status(201).send(mockEvent)); + + // Functionality + + const newEventData = { + name: 'New Event', + + project: 'projectXYZ', + + date: '2025-02-01T10:00:00Z', + }; + + const response = await request.post('/api/events/').send(newEventData); + + // Test + + expect(EventController.create).toHaveBeenCalledWith( + expect.objectContaining({ body: newEventData }), + + expect.anything(), + + expect.anything(), + ); + + expect(response.status).toBe(201); + + expect(response.body).toEqual(mockEvent); + + done(); + }); + }); + + describe('GET /api/events/:EventId (event_by_id)', () => { + it('should call EventController.event_by_id and return a specific event', async (done) => { + // Setup + + EventController.event_by_id.mockImplementationOnce((req, res) => + res.status(200).send(mockEvent), + ); + + // Functionality + + const response = await request.get(`/api/events/${mockEventId}`); + + // Test + + expect(EventController.event_by_id).toHaveBeenCalledWith( + expect.objectContaining({ params: { EventId: mockEventId } }), + + expect.anything(), + + expect.anything(), + ); + + expect(response.status).toBe(200); + + expect(response.body).toEqual(mockEvent); + + done(); + }); + }); + + describe('DELETE /api/events/:EventId (destroy)', () => { + it('should call EventController.destroy and return 204 No Content', async (done) => { + // Setup + + EventController.destroy.mockImplementationOnce((req, res) => res.status(204).send()); + + // Functionality + + const response = await request.delete(`/api/events/${mockEventId}`); + + // Test + + expect(EventController.destroy).toHaveBeenCalledWith( + expect.objectContaining({ params: { EventId: mockEventId } }), + + expect.anything(), + + expect.anything(), + ); + + expect(response.status).toBe(204); + + expect(response.body).toEqual({}); // 204 responses typically have an empty body + + done(); + }); + }); + + describe('PATCH /api/events/:EventId (update)', () => { + it('should call EventController.update and return the updated event', async (done) => { + // Setup + + EventController.update.mockImplementationOnce((req, res) => + res.status(200).send({ ...mockEvent, ...mockUpdatedEventData }), + ); + + // Functionality + + const response = await request.patch(`/api/events/${mockEventId}`).send(mockUpdatedEventData); + + // Test + + expect(EventController.update).toHaveBeenCalledWith( + expect.objectContaining({ + params: { EventId: mockEventId }, + + body: mockUpdatedEventData, + }), + + expect.anything(), + + expect.anything(), + ); + + expect(response.status).toBe(200); + + expect(response.body).toEqual({ ...mockEvent, ...mockUpdatedEventData }); + + done(); + }); + }); + + // TODO: Refactor and remove - Direct route implementation testing + + describe('GET /api/events/nexteventbyproject/:id', () => { + it('should return the last event for a given project ID directly from the router', async (done) => { + // Setup + + const mockEventsForProject = [ + { _id: 'eventA', project: mockProjectId, name: 'Event A' }, + + { _id: 'eventB', project: mockProjectId, name: 'Event B' }, + + { _id: 'eventC', project: mockProjectId, name: 'Event C' }, + ]; + + // Corrected Mongoose chaining mock for Event.find().populate().then() + + Event.find.mockImplementationOnce(() => ({ + populate: jest.fn().mockReturnThis(), // Returns 'this' (the mock object itself) + + then: jest.fn(function (callback) { + // This allows .then() to be called on the result of .populate() + + // and resolves the promise with the mock data. + + return Promise.resolve(callback(mockEventsForProject)); + }), + + catch: jest.fn(), // A no-op catch for success scenario + })); + + // Functionality + + const response = await request.get(`/api/events/nexteventbyproject/${mockProjectId}`); + + // Test + + expect(Event.find).toHaveBeenCalledWith({ project: mockProjectId }); + + // To test the populate call on the chained mock, we need to get the mock instance + + // that was returned by Event.find() and check its populate method. + + // Event.find.mock.results[0].value gives us the object returned by the first call to Event.find. + + expect(Event.find.mock.results[0].value.populate).toHaveBeenCalledWith('project'); + + expect(response.status).toBe(200); + + expect(response.body).toEqual(mockEventsForProject[mockEventsForProject.length - 1]); + + done(); + }); + + it('should return 500 if an error occurs when fetching next event by project', async (done) => { + // Setup + + const mockError = new Error('Simulated database error for next event by project'); + + // Corrected Mongoose chaining mock for Event.find().populate().then().catch() + + Event.find.mockImplementationOnce(() => ({ + populate: jest.fn().mockReturnThis(), + + then: jest.fn(() => Promise.reject(mockError)), // Simulate rejection + + catch: jest.fn(function (callback) { + // The catch block in the router should handle the rejection. + + // This mock ensures that the catch callback is invoked. + + return Promise.resolve(callback(mockError)); // You might want to return Promise.reject(mockError) + + // if you want to ensure the error propagates for other tests, + + // but here we just ensure the callback is hit. + }), + })); + + // Functionality + + const response = await request.get(`/api/events/nexteventbyproject/${mockProjectId}`); + + // Test + + expect(Event.find).toHaveBeenCalledWith({ project: mockProjectId }); + + expect(Event.find.mock.results[0].value.populate).toHaveBeenCalledWith('project'); + + expect(response.status).toBe(500); + + // No specific body expected for a 500, just the status + + done(); + }); + }); +}); From c7d2eaf609d4c8f0dac195687551439387f289f9 Mon Sep 17 00:00:00 2001 From: awlfccamp Date: Sun, 8 Jun 2025 21:33:10 -0500 Subject: [PATCH 2/4] clean up --- backend/routers/events.router.test.js | 110 +++----------------------- 1 file changed, 10 insertions(+), 100 deletions(-) diff --git a/backend/routers/events.router.test.js b/backend/routers/events.router.test.js index 4cf17d9ff..f83dc96c9 100644 --- a/backend/routers/events.router.test.js +++ b/backend/routers/events.router.test.js @@ -2,27 +2,13 @@ const express = require('express'); const supertest = require('supertest'); -// Mock the Event model (only used by the non-refactored route directly in the router) - -// Path corrected to go up one directory (backend/) then down into models/ - jest.mock('../models/event.model', () => ({ Event: { find: jest.fn(), - - // populate, then, and catch will be defined directly on the mock returned by find() - - // within the test cases for the direct router route to handle chaining. - - // For controller-driven routes, the controller itself is mocked. }, })); -const { Event } = require('../models/event.model'); // Path corrected - -// Mock the EventController - -// Path corrected to go up one directory (backend/) then down into controllers/ +const { Event } = require('../models/event.model'); jest.mock('../controllers', () => ({ EventController: { @@ -38,16 +24,10 @@ jest.mock('../controllers', () => ({ }, })); -const { EventController } = require('../controllers'); // Path corrected - -// Must import eventsRouter after setting up mocks for EventController - -// Path is relative to the current directory (routers/) +const { EventController } = require('../controllers'); const eventsRouter = require('./events.router'); -// Setup testapp with just eventsRouter which calls mocked EventController - const testapp = express(); testapp.use(express.json()); @@ -59,8 +39,6 @@ testapp.use('/api/events', eventsRouter); const request = supertest(testapp); describe('Unit Tests for events.router.js', () => { - // Mock event data for consistent testing - const mockEvent = { _id: 'event123', @@ -85,24 +63,18 @@ describe('Unit Tests for events.router.js', () => { describe('GET /api/events (event_list)', () => { it('should call EventController.event_list and return a list of events', async (done) => { - // Setup - EventController.event_list.mockImplementationOnce((req, res) => res.status(200).send([mockEvent]), ); - // Functionality - const response = await request.get('/api/events'); - // Test - expect(EventController.event_list).toHaveBeenCalledWith( - expect.anything(), // req + expect.anything(), - expect.anything(), // res + expect.anything(), - expect.anything(), // next + expect.anything(), ); expect(response.status).toBe(200); @@ -115,12 +87,8 @@ describe('Unit Tests for events.router.js', () => { describe('POST /api/events (create)', () => { it('should call EventController.create and return the created event', async (done) => { - // Setup - EventController.create.mockImplementationOnce((req, res) => res.status(201).send(mockEvent)); - // Functionality - const newEventData = { name: 'New Event', @@ -131,8 +99,6 @@ describe('Unit Tests for events.router.js', () => { const response = await request.post('/api/events/').send(newEventData); - // Test - expect(EventController.create).toHaveBeenCalledWith( expect.objectContaining({ body: newEventData }), @@ -151,18 +117,12 @@ describe('Unit Tests for events.router.js', () => { describe('GET /api/events/:EventId (event_by_id)', () => { it('should call EventController.event_by_id and return a specific event', async (done) => { - // Setup - EventController.event_by_id.mockImplementationOnce((req, res) => res.status(200).send(mockEvent), ); - // Functionality - const response = await request.get(`/api/events/${mockEventId}`); - // Test - expect(EventController.event_by_id).toHaveBeenCalledWith( expect.objectContaining({ params: { EventId: mockEventId } }), @@ -181,16 +141,10 @@ describe('Unit Tests for events.router.js', () => { describe('DELETE /api/events/:EventId (destroy)', () => { it('should call EventController.destroy and return 204 No Content', async (done) => { - // Setup - EventController.destroy.mockImplementationOnce((req, res) => res.status(204).send()); - // Functionality - const response = await request.delete(`/api/events/${mockEventId}`); - // Test - expect(EventController.destroy).toHaveBeenCalledWith( expect.objectContaining({ params: { EventId: mockEventId } }), @@ -201,7 +155,7 @@ describe('Unit Tests for events.router.js', () => { expect(response.status).toBe(204); - expect(response.body).toEqual({}); // 204 responses typically have an empty body + expect(response.body).toEqual({}); done(); }); @@ -209,18 +163,12 @@ describe('Unit Tests for events.router.js', () => { describe('PATCH /api/events/:EventId (update)', () => { it('should call EventController.update and return the updated event', async (done) => { - // Setup - EventController.update.mockImplementationOnce((req, res) => res.status(200).send({ ...mockEvent, ...mockUpdatedEventData }), ); - // Functionality - const response = await request.patch(`/api/events/${mockEventId}`).send(mockUpdatedEventData); - // Test - expect(EventController.update).toHaveBeenCalledWith( expect.objectContaining({ params: { EventId: mockEventId }, @@ -241,12 +189,8 @@ describe('Unit Tests for events.router.js', () => { }); }); - // TODO: Refactor and remove - Direct route implementation testing - describe('GET /api/events/nexteventbyproject/:id', () => { it('should return the last event for a given project ID directly from the router', async (done) => { - // Setup - const mockEventsForProject = [ { _id: 'eventA', project: mockProjectId, name: 'Event A' }, @@ -255,36 +199,20 @@ describe('Unit Tests for events.router.js', () => { { _id: 'eventC', project: mockProjectId, name: 'Event C' }, ]; - // Corrected Mongoose chaining mock for Event.find().populate().then() - Event.find.mockImplementationOnce(() => ({ - populate: jest.fn().mockReturnThis(), // Returns 'this' (the mock object itself) + populate: jest.fn().mockReturnThis(), then: jest.fn(function (callback) { - // This allows .then() to be called on the result of .populate() - - // and resolves the promise with the mock data. - return Promise.resolve(callback(mockEventsForProject)); }), - catch: jest.fn(), // A no-op catch for success scenario + catch: jest.fn(), })); - // Functionality - const response = await request.get(`/api/events/nexteventbyproject/${mockProjectId}`); - // Test - expect(Event.find).toHaveBeenCalledWith({ project: mockProjectId }); - // To test the populate call on the chained mock, we need to get the mock instance - - // that was returned by Event.find() and check its populate method. - - // Event.find.mock.results[0].value gives us the object returned by the first call to Event.find. - expect(Event.find.mock.results[0].value.populate).toHaveBeenCalledWith('project'); expect(response.status).toBe(200); @@ -295,44 +223,26 @@ describe('Unit Tests for events.router.js', () => { }); it('should return 500 if an error occurs when fetching next event by project', async (done) => { - // Setup - const mockError = new Error('Simulated database error for next event by project'); - // Corrected Mongoose chaining mock for Event.find().populate().then().catch() - Event.find.mockImplementationOnce(() => ({ populate: jest.fn().mockReturnThis(), - then: jest.fn(() => Promise.reject(mockError)), // Simulate rejection + then: jest.fn(() => Promise.reject(mockError)), catch: jest.fn(function (callback) { - // The catch block in the router should handle the rejection. - - // This mock ensures that the catch callback is invoked. - - return Promise.resolve(callback(mockError)); // You might want to return Promise.reject(mockError) - - // if you want to ensure the error propagates for other tests, - - // but here we just ensure the callback is hit. + return Promise.resolve(callback(mockError)); }), })); - // Functionality - const response = await request.get(`/api/events/nexteventbyproject/${mockProjectId}`); - // Test - expect(Event.find).toHaveBeenCalledWith({ project: mockProjectId }); expect(Event.find.mock.results[0].value.populate).toHaveBeenCalledWith('project'); expect(response.status).toBe(500); - // No specific body expected for a 500, just the status - done(); }); }); From 7500c86fe7b936636b5f7684de748ead4e5e812f Mon Sep 17 00:00:00 2001 From: awlfccamp Date: Sat, 12 Jul 2025 16:28:17 -0500 Subject: [PATCH 3/4] reuse mockEvent data --- backend/routers/events.router.test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/routers/events.router.test.js b/backend/routers/events.router.test.js index f83dc96c9..3da30da2c 100644 --- a/backend/routers/events.router.test.js +++ b/backend/routers/events.router.test.js @@ -90,11 +90,11 @@ describe('Unit Tests for events.router.js', () => { EventController.create.mockImplementationOnce((req, res) => res.status(201).send(mockEvent)); const newEventData = { - name: 'New Event', + name: mockEvent.name, - project: 'projectXYZ', + project: mockEvent.project, - date: '2025-02-01T10:00:00Z', + date: mockEvent.date, }; const response = await request.post('/api/events/').send(newEventData); From ab54814ebed7bdd9e2bde281a317f291472793f6 Mon Sep 17 00:00:00 2001 From: awlfccamp Date: Sat, 12 Jul 2025 16:44:03 -0500 Subject: [PATCH 4/4] remove redundant assertions and add comments --- backend/routers/events.router.test.js | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/backend/routers/events.router.test.js b/backend/routers/events.router.test.js index 3da30da2c..a8f4ed78c 100644 --- a/backend/routers/events.router.test.js +++ b/backend/routers/events.router.test.js @@ -1,7 +1,7 @@ const express = require('express'); const supertest = require('supertest'); - +// Mock the Mongoose Event model jest.mock('../models/event.model', () => ({ Event: { find: jest.fn(), @@ -9,7 +9,7 @@ jest.mock('../models/event.model', () => ({ })); const { Event } = require('../models/event.model'); - +//Mock the EventController to isolate router tests router tests fro controller logic jest.mock('../controllers', () => ({ EventController: { event_list: jest.fn(), @@ -27,7 +27,7 @@ jest.mock('../controllers', () => ({ const { EventController } = require('../controllers'); const eventsRouter = require('./events.router'); - +//Setup a test application const testapp = express(); testapp.use(express.json()); @@ -39,6 +39,7 @@ testapp.use('/api/events', eventsRouter); const request = supertest(testapp); describe('Unit Tests for events.router.js', () => { + //Mock data const mockEvent = { _id: 'event123', @@ -60,6 +61,7 @@ describe('Unit Tests for events.router.js', () => { afterEach(() => { jest.clearAllMocks(); }); + // Test suite for GET /api/events (event_list) describe('GET /api/events (event_list)', () => { it('should call EventController.event_list and return a list of events', async (done) => { @@ -84,7 +86,7 @@ describe('Unit Tests for events.router.js', () => { done(); }); }); - + // Test suite for POST /api/events (create) describe('POST /api/events (create)', () => { it('should call EventController.create and return the created event', async (done) => { EventController.create.mockImplementationOnce((req, res) => res.status(201).send(mockEvent)); @@ -114,7 +116,7 @@ describe('Unit Tests for events.router.js', () => { done(); }); }); - + // Test suite for GET /api/events/:EventId (event_by_id) describe('GET /api/events/:EventId (event_by_id)', () => { it('should call EventController.event_by_id and return a specific event', async (done) => { EventController.event_by_id.mockImplementationOnce((req, res) => @@ -138,7 +140,7 @@ describe('Unit Tests for events.router.js', () => { done(); }); }); - + // Test suite for DELETE /api/events/:EventId (destroy) describe('DELETE /api/events/:EventId (destroy)', () => { it('should call EventController.destroy and return 204 No Content', async (done) => { EventController.destroy.mockImplementationOnce((req, res) => res.status(204).send()); @@ -160,7 +162,7 @@ describe('Unit Tests for events.router.js', () => { done(); }); }); - + // Test suite for PATCH /api/events/:EventId (update) describe('PATCH /api/events/:EventId (update)', () => { it('should call EventController.update and return the updated event', async (done) => { EventController.update.mockImplementationOnce((req, res) => @@ -188,7 +190,7 @@ describe('Unit Tests for events.router.js', () => { done(); }); }); - + // Test suite for GET /api/events/nexteventbyproject/:id describe('GET /api/events/nexteventbyproject/:id', () => { it('should return the last event for a given project ID directly from the router', async (done) => { const mockEventsForProject = [ @@ -221,7 +223,7 @@ describe('Unit Tests for events.router.js', () => { done(); }); - + // Test case for error handling when fetching next event by project it('should return 500 if an error occurs when fetching next event by project', async (done) => { const mockError = new Error('Simulated database error for next event by project'); @@ -237,10 +239,6 @@ describe('Unit Tests for events.router.js', () => { const response = await request.get(`/api/events/nexteventbyproject/${mockProjectId}`); - expect(Event.find).toHaveBeenCalledWith({ project: mockProjectId }); - - expect(Event.find.mock.results[0].value.populate).toHaveBeenCalledWith('project'); - expect(response.status).toBe(500); done();