Skip to content
135 changes: 88 additions & 47 deletions apps/dashboard-api/src/__tests__/auth.controller.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,30 @@ jest.mock('@urbackend/common', () => {
create: jest.fn().mockResolvedValue(undefined),
};

class AppError extends Error {
constructor(statusCode, message) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
}
}

return {
Developer,
Otp,
Project,
PlatformEvent,
AppError,
ApiResponse: class ApiResponse {
constructor(data = {}, message = 'Success') {
this.data = data;
this.message = message;
this.success = true;
}
send(res, statusCode = 200) {
return res.status(statusCode).json({ success: this.success, data: this.data, message: this.message });
}
},
sendOtp: jest.fn().mockResolvedValue(undefined),
// Use real zod shapes so validation logic is exercised.
loginSchema: z.object({
Expand Down Expand Up @@ -98,7 +117,7 @@ jest.mock('@urbackend/common', () => {

const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const { Developer, Otp, Project, sendOtp } = require('@urbackend/common');
const { Developer, Otp, Project, sendOtp, AppError } = require('@urbackend/common');
const authController = require('../controllers/auth.controller');

// ---------------------------------------------------------------------------
Expand All @@ -124,6 +143,8 @@ const makeReq = (body = {}, user = null, cookies = {}) => ({
cookies,
});

const next = jest.fn();

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
Expand All @@ -149,11 +170,11 @@ describe('auth.controller', () => {
const req = makeReq({ email: 'new@example.com', password: 'password123' });
const res = makeRes();

await authController.register(req, res);
await authController.register(req, res, next);

expect(Developer.findOne).toHaveBeenCalledWith({ email: 'new@example.com' });
expect(res.status).toHaveBeenCalledWith(201);
expect(res.json).toHaveBeenCalledWith({ message: 'Registered successfully' });
expect(res.json).toHaveBeenCalledWith({ success: true, data: {}, message: 'Registered successfully' });
});

test('returns 400 when email already exists', async () => {
Expand All @@ -162,19 +183,20 @@ describe('auth.controller', () => {
const req = makeReq({ email: 'existing@example.com', password: 'password123' });
const res = makeRes();

await authController.register(req, res);
await authController.register(req, res, next);

expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith({ error: 'Email already exists' });
expect(next).toHaveBeenCalledWith(expect.any(AppError));
expect(next.mock.calls[0][0].statusCode).toBe(400);
});

test('returns 400 on Zod validation error (invalid email)', async () => {
const req = makeReq({ email: 'not-an-email', password: 'password123' });
const res = makeRes();

await authController.register(req, res);
await authController.register(req, res, next);

expect(res.status).toHaveBeenCalledWith(400);
expect(next).toHaveBeenCalledWith(expect.any(AppError));
expect(next.mock.calls[0][0].statusCode).toBe(400);
});
});

Expand All @@ -199,7 +221,7 @@ describe('auth.controller', () => {
const req = makeReq({ email: 'test@example.com', password: 'correctpass' });
const res = makeRes();

await authController.login(req, res);
await authController.login(req, res, next);

expect(bcrypt.compare).toHaveBeenCalledWith('correctpass', 'hashed_password');
expect(res.status).toHaveBeenCalledWith(200);
Expand All @@ -216,10 +238,11 @@ describe('auth.controller', () => {
const req = makeReq({ email: 'noone@example.com', password: 'pass' });
const res = makeRes();

await authController.login(req, res);
await authController.login(req, res, next);

expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith({ error: 'User not found' });
expect(next).toHaveBeenCalledWith(expect.any(AppError));
expect(next.mock.calls[0][0].statusCode).toBe(400);
expect(next.mock.calls[0][0].message).toBe('User not found');
});

test('returns 400 on invalid password', async () => {
Expand All @@ -229,19 +252,21 @@ describe('auth.controller', () => {
const req = makeReq({ email: 'test@example.com', password: 'wrongpass' });
const res = makeRes();

await authController.login(req, res);
await authController.login(req, res, next);

expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith({ error: 'Invalid password' });
expect(next).toHaveBeenCalledWith(expect.any(AppError));
expect(next.mock.calls[0][0].statusCode).toBe(400);
expect(next.mock.calls[0][0].message).toBe('Invalid password');
});

test('returns 400 on Zod validation error (missing password)', async () => {
const req = makeReq({ email: 'test@example.com' });
const res = makeRes();

await authController.login(req, res);
await authController.login(req, res, next);

expect(res.status).toHaveBeenCalledWith(400);
expect(next).toHaveBeenCalledWith(expect.any(AppError));
expect(next.mock.calls[0][0].statusCode).toBe(400);
});
});

Expand All @@ -266,7 +291,7 @@ describe('auth.controller', () => {
const req = makeReq({}, null, { refreshToken: 'valid_refresh_token' });
const res = makeRes();

await authController.refreshToken(req, res);
await authController.refreshToken(req, res, next);

expect(jwt.verify).toHaveBeenCalledWith('valid_refresh_token', 'refresh-secret');
expect(res.status).toHaveBeenCalledWith(200);
Expand All @@ -276,21 +301,27 @@ describe('auth.controller', () => {
const req = makeReq({}, null, {});
const res = makeRes();

await authController.refreshToken(req, res);
await authController.refreshToken(req, res, next);

expect(res.status).toHaveBeenCalledWith(401);
expect(res.json).toHaveBeenCalledWith({ error: 'No refresh token provided' });
expect(next).toHaveBeenCalledWith(expect.any(AppError));
expect(next.mock.calls[0][0].statusCode).toBe(401);
expect(next.mock.calls[0][0].message).toBe('No refresh token provided');
});

test('returns 403 when refresh token is invalid (jwt.verify throws)', async () => {
jwt.verify.mockImplementation(() => { throw new Error('invalid token'); });
jwt.verify.mockImplementation(() => {
const err = new Error('invalid token');
err.name = 'JsonWebTokenError';
throw err;
});

const req = makeReq({}, null, { refreshToken: 'bad_token' });
const res = makeRes();

await authController.refreshToken(req, res);
await authController.refreshToken(req, res, next);

expect(res.status).toHaveBeenCalledWith(403);
expect(next).toHaveBeenCalledWith(expect.any(AppError));
expect(next.mock.calls[0][0].statusCode).toBe(403);
});

test('returns 403 when stored refresh token does not match', async () => {
Expand All @@ -300,10 +331,11 @@ describe('auth.controller', () => {
const req = makeReq({}, null, { refreshToken: 'valid_refresh_token' });
const res = makeRes();

await authController.refreshToken(req, res);
await authController.refreshToken(req, res, next);

expect(res.status).toHaveBeenCalledWith(403);
expect(res.json).toHaveBeenCalledWith({ error: 'Invalid refresh token' });
expect(next).toHaveBeenCalledWith(expect.any(AppError));
expect(next.mock.calls[0][0].statusCode).toBe(403);
expect(next.mock.calls[0][0].message).toBe('Invalid refresh token');
});
});

Expand All @@ -320,13 +352,14 @@ describe('auth.controller', () => {
const req = makeReq({}, { _id: 'dev_id_1' });
const res = makeRes();

await authController.logout(req, res);
await authController.logout(req, res, next);

expect(mockUser.save).toHaveBeenCalled();
expect(mockUser.refreshToken).toBeNull();
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith({
success: true,
data: {},
message: 'Logged out successfully',
});
});
Expand All @@ -335,7 +368,7 @@ describe('auth.controller', () => {
const req = makeReq();
const res = makeRes();

await authController.logout(req, res);
await authController.logout(req, res, next);

expect(res.status).toHaveBeenCalledWith(200);
});
Expand All @@ -353,7 +386,7 @@ describe('auth.controller', () => {
const req = makeReq({}, { _id: 'dev_id_1' });
const res = makeRes();

await authController.getMe(req, res);
await authController.getMe(req, res, next);

expect(Developer.findById).toHaveBeenCalledWith('dev_id_1');
expect(mockSelect).toHaveBeenCalledWith('-password -refreshToken');
Expand All @@ -370,10 +403,11 @@ describe('auth.controller', () => {
const req = makeReq({}, { _id: 'missing_id' });
const res = makeRes();

await authController.getMe(req, res);
await authController.getMe(req, res, next);

expect(res.status).toHaveBeenCalledWith(404);
expect(res.json).toHaveBeenCalledWith({ error: 'User not found' });
expect(next).toHaveBeenCalledWith(expect.any(AppError));
expect(next.mock.calls[0][0].statusCode).toBe(404);
expect(next.mock.calls[0][0].message).toBe('User not found');
});
});

Expand All @@ -396,11 +430,13 @@ describe('auth.controller', () => {
);
const res = makeRes();

await authController.changePassword(req, res);
await authController.changePassword(req, res, next);

expect(bcrypt.compare).toHaveBeenCalledWith('oldpass', 'old_hashed');
expect(mockUser.save).toHaveBeenCalled();
expect(res.json).toHaveBeenCalledWith({
success: true,
data: {},
message: 'Password updated successfully',
});
});
Expand All @@ -418,10 +454,11 @@ describe('auth.controller', () => {
);
const res = makeRes();

await authController.changePassword(req, res);
await authController.changePassword(req, res, next);

expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith({ error: 'Incorrect current password' });
expect(next).toHaveBeenCalledWith(expect.any(AppError));
expect(next.mock.calls[0][0].statusCode).toBe(400);
expect(next.mock.calls[0][0].message).toBe('Incorrect current password');
});
});

Expand All @@ -433,10 +470,11 @@ describe('auth.controller', () => {
const req = makeReq({ email: 'noone@example.com' });
const res = makeRes();

await authController.sendOtp(req, res);
await authController.sendOtp(req, res, next);

expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith({ error: 'User not found. Ensure you are using the correct email.' });
expect(next).toHaveBeenCalledWith(expect.any(AppError));
expect(next.mock.calls[0][0].statusCode).toBe(400);
expect(next.mock.calls[0][0].message).toBe('User not found. Ensure you are using the correct email.');
});

test('returns 400 when user is already verified', async () => {
Expand All @@ -445,10 +483,11 @@ describe('auth.controller', () => {
const req = makeReq({ email: 'verified@example.com' });
const res = makeRes();

await authController.sendOtp(req, res);
await authController.sendOtp(req, res, next);

expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith({ error: 'Account is already verified. Please login.' });
expect(next).toHaveBeenCalledWith(expect.any(AppError));
expect(next.mock.calls[0][0].statusCode).toBe(400);
expect(next.mock.calls[0][0].message).toBe('Account is already verified. Please login.');
});

test('sends OTP and returns success for unverified user', async () => {
Expand All @@ -464,10 +503,10 @@ describe('auth.controller', () => {
const req = makeReq({ email: 'unverified@example.com' });
const res = makeRes();

await authController.sendOtp(req, res);
await authController.sendOtp(req, res, next);

expect(sendOtp).toHaveBeenCalled();
expect(res.json).toHaveBeenCalledWith({ message: 'OTP sent successfully' });
expect(res.json).toHaveBeenCalledWith({ success: true, data: {}, message: 'OTP sent successfully' });
});
});

Expand All @@ -479,7 +518,7 @@ describe('auth.controller', () => {
const req = makeReq({ email: 'ghost@example.com' });
const res = makeRes();

await authController.forgotPassword(req, res);
await authController.forgotPassword(req, res, next);

expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith(
Expand Down Expand Up @@ -518,7 +557,7 @@ describe('auth.controller', () => {
});
const res = makeRes();

await authController.resetPassword(req, res);
await authController.resetPassword(req, res, next);

expect(mockUser.refreshToken).toBeNull();
expect(mockUser.save).toHaveBeenCalled();
Expand All @@ -534,6 +573,8 @@ describe('auth.controller', () => {
);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith({
success: true,
data: {},
message: 'Password reset successfully. Please log in with your new password.'
});
});
Expand Down
Loading
Loading