Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
const mongoose = require('mongoose');
const BuildingProject = require('../../../models/bmdashboard/buildingProject');
const toolAvailabilityController = require('../toolAvailabilityController');

const projectId = '65419e61105441587e2dec99';

const makeResponse = () => ({
status: jest.fn().mockReturnThis(),
json: jest.fn().mockReturnThis(),
});

describe('Tool Availability Controller', () => {
let ToolAvailability;
let controller;

beforeEach(() => {
jest.restoreAllMocks();

jest.spyOn(BuildingProject, 'find');

ToolAvailability = {
aggregate: jest.fn(),
distinct: jest.fn(),
};

controller = toolAvailabilityController(ToolAvailability);
});

afterEach(() => {
jest.restoreAllMocks();
});

describe('getToolsAvailability', () => {
it('returns 400 for an invalid project ID', async () => {
const req = { params: { id: 'invalid-id' }, query: {} };
const res = makeResponse();

await controller.getToolsAvailability(req, res);

expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith({
error: 'Invalid project ID format',
});
expect(ToolAvailability.aggregate).not.toHaveBeenCalled();
});

it('returns aggregated tool availability data', async () => {
const data = [
{
toolName: 'Hammer',
inUse: 5,
needsReplacement: 2,
yetToReceive: 1,
},
];

ToolAvailability.aggregate.mockResolvedValue(data);

const req = { params: { id: projectId }, query: {} };
const res = makeResponse();

await controller.getToolsAvailability(req, res);

expect(ToolAvailability.aggregate).toHaveBeenCalledWith(
expect.arrayContaining([
expect.objectContaining({
$match: {
projectId: new mongoose.Types.ObjectId(projectId),
},
}),
]),
);
expect(res.json).toHaveBeenCalledWith(data);
});

it('applies both start and end date filters', async () => {
ToolAvailability.aggregate.mockResolvedValue([]);

const req = {
params: { id: projectId },
query: {
startDate: '2026-01-01',
endDate: '2026-01-31',
},
};
const res = makeResponse();

await controller.getToolsAvailability(req, res);

const pipeline = ToolAvailability.aggregate.mock.calls[0][0];

expect(pipeline[0].$match.date).toEqual({
$gte: new Date('2026-01-01'),
$lte: new Date('2026-01-31'),
});
});

it('applies only the start date when end date is absent', async () => {
ToolAvailability.aggregate.mockResolvedValue([]);

const req = {
params: { id: projectId },
query: { startDate: '2026-01-01' },
};
const res = makeResponse();

await controller.getToolsAvailability(req, res);

const pipeline = ToolAvailability.aggregate.mock.calls[0][0];

expect(pipeline[0].$match.date).toEqual({
$gte: new Date('2026-01-01'),
});
});

it('applies only the end date when start date is absent', async () => {
ToolAvailability.aggregate.mockResolvedValue([]);

const req = {
params: { id: projectId },
query: { endDate: '2026-01-31' },
};
const res = makeResponse();

await controller.getToolsAvailability(req, res);

const pipeline = ToolAvailability.aggregate.mock.calls[0][0];

expect(pipeline[0].$match.date).toEqual({
$lte: new Date('2026-01-31'),
});
});

it('returns an empty array when no data exists', async () => {
ToolAvailability.aggregate.mockResolvedValue([]);

const req = { params: { id: projectId }, query: {} };
const res = makeResponse();

await controller.getToolsAvailability(req, res);

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

it('handles database aggregation failures', async () => {
ToolAvailability.aggregate.mockRejectedValue(new Error('Database failure'));

const req = { params: { id: projectId }, query: {} };
const res = makeResponse();

const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});

try {
await controller.getToolsAvailability(req, res);

expect(res.status).toHaveBeenCalledWith(500);
expect(res.json).toHaveBeenCalledWith({
error: 'Internal server error',
});
} finally {
consoleSpy.mockRestore();
}
});
});

describe('getUniqueProjectIds', () => {
it('returns project names for valid project IDs', async () => {
ToolAvailability.distinct.mockResolvedValue([new mongoose.Types.ObjectId(projectId)]);

const projects = [
{
_id: new mongoose.Types.ObjectId(projectId),
name: 'Building 1',
},
];

const query = {
select: jest.fn().mockReturnThis(),
sort: jest.fn().mockReturnThis(),
lean: jest.fn().mockResolvedValue(projects),
};

BuildingProject.find.mockReturnValue(query);

const req = {};
const res = makeResponse();

await controller.getUniqueProjectIds(req, res);

expect(BuildingProject.find).toHaveBeenCalledWith({
_id: {
$in: [new mongoose.Types.ObjectId(projectId)],
},
});

expect(res.json).toHaveBeenCalledWith([
{
projectId: projects[0]._id,
projectName: 'Building 1',
},
]);
});

it('returns an empty array when no project IDs exist', async () => {
ToolAvailability.distinct.mockResolvedValue([]);

const res = makeResponse();

await controller.getUniqueProjectIds({}, res);

expect(res.json).toHaveBeenCalledWith([]);
expect(BuildingProject.find).not.toHaveBeenCalled();
});

it('ignores invalid project IDs', async () => {
ToolAvailability.distinct.mockResolvedValue(['invalid-id']);

const res = makeResponse();

await controller.getUniqueProjectIds({}, res);

expect(res.json).toHaveBeenCalledWith([]);
expect(BuildingProject.find).not.toHaveBeenCalled();
});

it('handles project lookup failures', async () => {
ToolAvailability.distinct.mockRejectedValue(new Error('Database failure'));

const res = makeResponse();

const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});

try {
await controller.getUniqueProjectIds({}, res);

expect(res.status).toHaveBeenCalledWith(500);
expect(res.json).toHaveBeenCalledWith({
error: 'Internal server error',
});
} finally {
consoleSpy.mockRestore();
}
});
});
});
52 changes: 22 additions & 30 deletions src/controllers/bmdashboard/toolAvailabilityController.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
const { ObjectId } = require('mongoose').Types;
const mongoose = require('mongoose');
const BuildingProject = require('../../models/bmdashboard/buildingProject');

const { ObjectId } = mongoose.Types;
const toolAvailabilityController = function (ToolAvailability) {
const getToolsAvailability = async (req, res) => {
try {
Expand Down Expand Up @@ -124,36 +126,26 @@ const toolAvailabilityController = function (ToolAvailability) {

const getUniqueProjectIds = async (req, res) => {
try {
// Use aggregation to get distinct project IDs and lookup their names
const results = await ToolAvailability.aggregate([
{
$group: {
_id: '$projectId',
},
},
{
$lookup: {
from: 'buildingProject',
localField: '_id',
foreignField: '_id',
as: 'projectDetails',
},
},
{
$project: {
_id: 1,
projectName: { $arrayElemAt: ['$projectDetails.projectName', 0] },
},
},
{
$sort: { projectName: 1 },
},
]);
const projectIds = await ToolAvailability.distinct('projectId');

const validProjectIds = projectIds
.filter((id) => ObjectId.isValid(id))
.map((id) => new ObjectId(String(id)));

if (validProjectIds.length === 0) {
return res.json([]);
}

const projects = await BuildingProject.find({
_id: { $in: validProjectIds },
})
.select('_id name')
.sort({ name: 1 })
.lean();

// Format the response
const formattedResults = results.map((item) => ({
projectId: item._id,
projectName: item.projectName || 'Unknown Project',
const formattedResults = projects.map((project) => ({
projectId: project._id,
projectName: project.name,
}));

return res.json(formattedResults);
Expand Down
Loading
Loading