Skip to content
Merged
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
146 changes: 146 additions & 0 deletions src/__tests__/project.routes.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
const jwt = require('jsonwebtoken');
const request = require('supertest');

jest.mock('../models/Project.model', () => ({
findById: jest.fn(),
}));

jest.mock('../models/User.model', () => ({
findById: jest.fn(),
}));

const Project = require('../models/Project.model');
const User = require('../models/User.model');
const app = require('../app');

describe('GET /api/projects/:id', () => {
const projectId = '507f1f77bcf86cd799439066';
const ownerId = '507f1f77bcf86cd799439055';
const adminId = '507f1f77bcf86cd799439099';

const projectDoc = {
_id: projectId,
title: 'Campaign Test',
description: 'A valid active campaign',
owner: { _id: ownerId, fullName: 'Owner Name' },
isActive: true,
documents: [],
toObject: function () {
return {
_id: this._id,
title: this.title,
description: this.description,
owner: this.owner,
isActive: this.isActive,
documents: this.documents,
};
},
};

beforeEach(() => {
jest.clearAllMocks();
User.findById.mockImplementation((id) => {
if (id === ownerId) {
return Promise.resolve({ _id: ownerId, role: 'user' });
}
if (id === adminId) {
return Promise.resolve({ _id: adminId, role: 'admin' });
}
return Promise.resolve(null);
});
});

it('returns full project details for a valid active project id without auth', async () => {
Project.findById.mockReturnValueOnce({
populate: jest.fn().mockReturnThis(),
exec: jest.fn().mockResolvedValueOnce(projectDoc),
});

const response = await request(app).get(`/api/projects/${projectId}`).expect(200);

expect(response.body.success).toBe(true);
expect(response.body.message).toBe('Project retrieved successfully');
expect(response.body.data.project).toMatchObject({
_id: projectId,
title: 'Campaign Test',
description: 'A valid active campaign',
owner: { fullName: 'Owner Name' },
isActive: true,
});
});

it('returns 404 for a non-existent project id', async () => {
Project.findById.mockReturnValueOnce({
populate: jest.fn().mockReturnThis(),
exec: jest.fn().mockResolvedValueOnce(null),
});

const response = await request(app).get(`/api/projects/${projectId}`).expect(404);

expect(response.body.success).toBe(false);
expect(response.body.message).toBe('Project not found');
});

it('returns 404 for an inactive project if requester is not owner/admin', async () => {
Project.findById.mockReturnValueOnce({
populate: jest.fn().mockReturnThis(),
exec: jest.fn().mockResolvedValueOnce({
...projectDoc,
isActive: false,
}),
});

const response = await request(app).get(`/api/projects/${projectId}`).expect(404);

expect(response.body.success).toBe(false);
expect(response.body.message).toBe('Project not found');
});

it('returns inactive project details to owner', async () => {
const token = jwt.sign(
{ sub: ownerId, type: 'access' },
process.env.JWT_SECRET || 'test-secret',
{ expiresIn: '1h' }
);

Project.findById.mockReturnValueOnce({
populate: jest.fn().mockReturnThis(),
exec: jest.fn().mockResolvedValueOnce({
...projectDoc,
isActive: false,
}),
});

const response = await request(app)
.get(`/api/projects/${projectId}`)
.set('Authorization', `Bearer ${token}`)
.expect(200);

expect(response.body.success).toBe(true);
expect(response.body.data.project.owner).toEqual({ fullName: 'Owner Name' });
});

it('returns inactive project details to admin', async () => {
const token = jwt.sign(
{ sub: adminId, type: 'access' },
process.env.JWT_SECRET || 'test-secret',
{ expiresIn: '1h' }
);

Project.findById.mockReturnValueOnce({
populate: jest.fn().mockReturnThis(),
exec: jest.fn().mockResolvedValueOnce({
...projectDoc,
isActive: false,
}),
});

const response = await request(app)
.get(`/api/projects/${projectId}`)
.set('Authorization', `Bearer ${token}`)
.expect(200);

expect(response.body.success).toBe(true);
expect(response.body.data.project.owner).toEqual({ fullName: 'Owner Name' });
});
});
14 changes: 13 additions & 1 deletion src/controllers/project.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ const getProjectDetails = async (req, res, next) => {
return next(error);
}

const ownerId = project.owner && project.owner._id ? project.owner._id.toString() : project.owner?.toString();
const isOwner = req.userId && ownerId === req.userId;
const isAdmin = req.user?.role === 'admin';

if (project.isActive === false && !isOwner && !isAdmin) {
const isActive = project.status !== 'inactive';
const isOwner = req.userId && project.owner && project.owner._id?.toString() === req.userId;
const isAdmin = req.user && req.user.role === 'admin';
Expand All @@ -28,6 +33,12 @@ const getProjectDetails = async (req, res, next) => {
return next(error);
}

const responseProject = project.toObject();
if (responseProject.owner && responseProject.owner.fullName) {
responseProject.owner = { fullName: responseProject.owner.fullName };
}

return sendSuccess(res, { project: responseProject }, 200, 'Project retrieved successfully');
const projectData = project.toObject({ getters: true });
projectData.owner = project.owner ? { fullName: project.owner.fullName } : null;

Expand Down Expand Up @@ -105,4 +116,5 @@ const uploadDocuments = async (req, res, next) => {
}
};

module.exports = { getProjectDetails, uploadDocuments };
module.exports = { getProjectById, uploadDocuments };
module.exports = { getProjectDetails, uploadDocuments };
31 changes: 31 additions & 0 deletions src/middlewares/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,35 @@ const authenticate = async (req, res, next) => {
}
};

const optionalAuthenticate = async (req, res, next) => {
try {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return next();
}

const token = authHeader.substring(7);
if (!token) {
return next();
}

const decoded = jwt.verify(token, process.env.JWT_SECRET);
if (decoded.type !== 'access') {
return next();
}

const user = await User.findById(decoded.sub);
if (!user) {
return next();
}

req.user = user;
req.userId = user._id.toString();
next();
} catch (_error) {
next();
}
};

module.exports = authenticate;
module.exports.optionalAuthenticate = optionalAuthenticate;
1 change: 1 addition & 0 deletions src/models/Project.model.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const projectSchema = new mongoose.Schema(
title: { type: String, required: true, trim: true },
description: { type: String, trim: true },
owner: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
isActive: { type: Boolean, default: true },
documents: { type: [documentSchema], default: [] },
status: { type: String, enum: ['active', 'inactive'], default: 'active' },
// ... add your other project fields here
Expand Down
9 changes: 9 additions & 0 deletions src/routes/project.routes.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
const express = require('express');
const router = express.Router();
const authenticate = require('../middlewares/auth');
const { optionalAuthenticate } = require('../middlewares/auth');
const { handleUpload } = require('../middlewares/upload.middleware');
const { getProjectById, uploadDocuments } = require('../controllers/project.controller');

/**
* GET /api/projects/:id
* Retrieve a single project by id
*/
router.get('/:id', optionalAuthenticate, getProjectById);
const authenticateOptional = require('../middlewares/auth.optional');
const { handleUpload } = require('../middlewares/upload.middleware');
const { uploadDocuments, getProjectDetails } = require('../controllers/project.controller');
Expand Down
Loading