Skip to content
Open
190 changes: 190 additions & 0 deletions src/controllers/__tests__/activityLogController.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
const mockSelect = jest.fn();
const mockSort = jest.fn(() => ({ select: mockSelect }));
const mockFind = jest.fn(() => ({ sort: mockSort }));

jest.mock('../../models/activityLog', () => ({
find: (...args) => mockFind(...args),
}));

const mongoose = require('mongoose');
const controller = require('../activityLogController')();

describe('activityLogController', () => {
let req;
let res;

beforeEach(() => {
jest.clearAllMocks();
res = {
status: jest.fn().mockReturnThis(),
json: jest.fn(),
};
});

describe('fetchStudentDailyLog', () => {
beforeEach(() => {
req = {
body: {
requestor: { requestorId: new mongoose.Types.ObjectId().toString() },
},
query: {},
};
});

it('should return logs for the requestor', async () => {
const mockLogs = [{ action_type: 'comment', created_at: new Date() }];
mockSelect.mockResolvedValue(mockLogs);

await controller.fetchStudentDailyLog(req, res);

expect(res.json).toHaveBeenCalledWith(mockLogs);
});

it('should return 403 if requested studentId differs from requestorId', async () => {
req.query.studentId = new mongoose.Types.ObjectId().toString();

await controller.fetchStudentDailyLog(req, res);

expect(res.status).toHaveBeenCalledWith(403);
expect(res.json).toHaveBeenCalledWith({
error: "Forbidden: Cannot access another student's log",
});
});

it('should return 200 if requested studentId matches requestorId', async () => {
req.query.studentId = req.body.requestor.requestorId;
mockSelect.mockResolvedValue([]);

await controller.fetchStudentDailyLog(req, res);

expect(res.json).toHaveBeenCalledWith([]);
});

it('should return 400 for invalid studentId format', async () => {
req.body.requestor.requestorId = 'not-a-valid-id';

await controller.fetchStudentDailyLog(req, res);

expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith({ error: 'Invalid studentId format' });
});

it('should return 500 on database error', async () => {
mockSelect.mockRejectedValue(new Error('DB error'));

await controller.fetchStudentDailyLog(req, res);

expect(res.status).toHaveBeenCalledWith(500);
expect(res.json).toHaveBeenCalledWith({ error: 'DB error' });
});

it('should return 500 when req.body.requestor is undefined', async () => {
req.body = {};

await controller.fetchStudentDailyLog(req, res);

expect(res.status).toHaveBeenCalledWith(500);
expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ error: expect.any(String) }));
});

it('should use requestorId when query studentId is empty string', async () => {
req.query.studentId = '';
mockSelect.mockResolvedValue([{ action_type: 'comment' }]);

await controller.fetchStudentDailyLog(req, res);

expect(mockFind).toHaveBeenCalledWith({ actor_id: expect.any(mongoose.Types.ObjectId) });
expect(res.json).toHaveBeenCalledWith([{ action_type: 'comment' }]);
});

it('should call find with correct filter, sort, and select', async () => {
mockSelect.mockResolvedValue([]);

await controller.fetchStudentDailyLog(req, res);

expect(mockFind).toHaveBeenCalledWith({ actor_id: expect.any(mongoose.Types.ObjectId) });
expect(mockSort).toHaveBeenCalledWith({ created_at: -1 });
expect(mockSelect).toHaveBeenCalledWith('action_type metadata created_at actor_id');
});
});

describe('fetchEducatorDailyLog', () => {
beforeEach(() => {
req = {
params: { studentId: new mongoose.Types.ObjectId().toString() },
};
});

it('should return logs for the given studentId', async () => {
const mockLogs = [{ action_type: 'note', created_at: new Date() }];
mockSelect.mockResolvedValue(mockLogs);

await controller.fetchEducatorDailyLog(req, res);

expect(res.json).toHaveBeenCalledWith(mockLogs);
});

it('should return 400 when studentId is missing', async () => {
req.params = {};

await controller.fetchEducatorDailyLog(req, res);

expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith({ error: 'Missing studentId' });
});

it('should return 400 for invalid studentId format', async () => {
req.params.studentId = 'invalid-id';

await controller.fetchEducatorDailyLog(req, res);

expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith({ error: 'Invalid studentId format' });
});

it('should return 500 on database error', async () => {
mockSelect.mockRejectedValue(new Error('DB failure'));

await controller.fetchEducatorDailyLog(req, res);

expect(res.status).toHaveBeenCalledWith(500);
expect(res.json).toHaveBeenCalledWith({ error: 'DB failure' });
});

it('should return empty array when no logs exist', async () => {
mockSelect.mockResolvedValue([]);

await controller.fetchEducatorDailyLog(req, res);

expect(res.json).toHaveBeenCalledWith([]);
});

it('should return 400 when studentId is empty string', async () => {
req.params.studentId = '';

await controller.fetchEducatorDailyLog(req, res);

expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith({ error: 'Missing studentId' });
});

it('should return 400 when studentId is null', async () => {
req.params.studentId = null;

await controller.fetchEducatorDailyLog(req, res);

expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith({ error: 'Missing studentId' });
});

it('should call find with correct filter, sort, and select', async () => {
mockSelect.mockResolvedValue([]);

await controller.fetchEducatorDailyLog(req, res);

expect(mockFind).toHaveBeenCalledWith({ actor_id: expect.any(mongoose.Types.ObjectId) });
expect(mockSort).toHaveBeenCalledWith({ created_at: -1 });
expect(mockSelect).toHaveBeenCalledWith('action_type metadata created_at actor_id');
});
});
});
62 changes: 62 additions & 0 deletions src/controllers/activityLogController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
const mongoose = require('mongoose');
const ActivityLog = require('../models/activityLog');

const activityLogController = function () {
async function fetchStudentDailyLog(req, res) {
try {
const studentId = req.body.requestor.requestorId;

const requestedStudentId = req.query.studentId;
if (requestedStudentId && requestedStudentId !== String(studentId)) {
return res.status(403).json({ error: "Forbidden: Cannot access another student's log" });
}

const authorizedStudentId = requestedStudentId || String(studentId);

let objectId;
try {
objectId = new mongoose.Types.ObjectId(authorizedStudentId);
} catch (e) {
return res.status(400).json({ error: 'Invalid studentId format' });
}

const logs = await ActivityLog.find({ actor_id: objectId })
.sort({ created_at: -1 })
.select('action_type metadata created_at actor_id');

res.json(logs);
} catch (err) {
res.status(500).json({ error: err.message });
}
}

async function fetchEducatorDailyLog(req, res) {
try {
const { studentId } = req.params;

if (!studentId) return res.status(400).json({ error: 'Missing studentId' });

let objectId;
try {
objectId = new mongoose.Types.ObjectId(studentId);
} catch (e) {
return res.status(400).json({ error: 'Invalid studentId format' });
}

const logs = await ActivityLog.find({ actor_id: objectId })
.sort({ created_at: -1 })
.select('action_type metadata created_at actor_id');

res.json(logs);
} catch (err) {
res.status(500).json({ error: err.message });
}
}

return {
fetchStudentDailyLog,
fetchEducatorDailyLog,
};
};

module.exports = activityLogController;
51 changes: 51 additions & 0 deletions src/models/activityLog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
const mongoose = require('mongoose');
const { v4: uuidv4, validate: isUUID } = require('uuid');

const activityLogSchema = new mongoose.Schema(
{
log_id: {
type: mongoose.Schema.Types.ObjectId,
default: () => new mongoose.Types.ObjectId(),
},
actor_id: {
type: mongoose.Schema.Types.ObjectId,
ref: 'UserProfile',
required: true,
},
action_type: {
type: String,
enum: ['comment', 'note', 'announcement', 'task_upload', 'task_complete'],
required: true,
},
entity_id: {
type: String,
required: true,
default() {
return uuidv4();
},
validate: {
validator(v) {
return isUUID(v);
},
message(props) {
return `${props.value} is not a valid UUID!`;
},
},
},
metadata: {
type: mongoose.Schema.Types.Mixed,
default: {},
},
created_at: {
type: Date,
default: Date.now,
},
},
{
collection: 'ActivityLog',
versionKey: false,
},
);

const ActivityLog = mongoose.model('ActivityLog', activityLogSchema);
module.exports = ActivityLog;
13 changes: 13 additions & 0 deletions src/routes/activityLogRouter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const express = require('express');

const routes = function () {
const activityLogRouter = express.Router();
const controller = require('../controllers/activityLogController')();
activityLogRouter.route('/student/daily-log').get(controller.fetchStudentDailyLog);

activityLogRouter.route('/educator/daily-log/:studentId').get(controller.fetchEducatorDailyLog);

return activityLogRouter;
};

module.exports = routes;
1 change: 0 additions & 1 deletion src/routes/kitchenandinventory/KIInventoryRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,3 @@ const router = function () {
};

module.exports = router;

6 changes: 3 additions & 3 deletions src/scripts/seedKIInventoryItems.js
Original file line number Diff line number Diff line change
Expand Up @@ -470,12 +470,12 @@ async function seed() {

// Quick stats summary
const total = inserted.length;
const critical = inserted.filter(i => i.presentQuantity <= i.reorderAt * 0.5).length;
const critical = inserted.filter((i) => i.presentQuantity <= i.reorderAt * 0.5).length;
const low = inserted.filter(
i => i.presentQuantity <= i.reorderAt && i.presentQuantity > i.reorderAt * 0.5,
(i) => i.presentQuantity <= i.reorderAt && i.presentQuantity > i.reorderAt * 0.5,
).length;
const preserved = inserted.filter(
i =>
(i) =>
i.category === 'INGREDIENT' &&
new Date(i.expiryDate) >= new Date(Date.now() + 365 * 24 * 60 * 60 * 1000),
).length;
Expand Down
3 changes: 3 additions & 0 deletions src/startup/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,8 @@ const jobHitsAndApplicationsRoutes = require('../routes/jobAnalytics/JobHitsAndA
// Education Portal
const educatorRoutes = require('../routes/educatorRoutes');

const activityLogRouter = require('../routes/activityLogRouter')();

module.exports = function (app) {
app.use('/api/bm/summary-dashboard', summaryDashboardRouter);
app.use('/api', forgotPwdRouter);
Expand Down Expand Up @@ -638,6 +640,7 @@ module.exports = function (app) {
app.use('/api/lb', bidDeadlinesRouter);
app.use('/api/lb', SMSRouter);

app.use('/api/', activityLogRouter);
// Education Portal
app.use('/api/educationportal/educator', educatorRoutes);
app.use('/api', materialCostRouter);
Expand Down
Loading