email verfication after account creation, password component, added password special characters
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
const { authenticateToken } = require('../../../middleware/auth');
|
||||
const { authenticateToken, requireVerifiedEmail } = require('../../../middleware/auth');
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
jest.mock('jsonwebtoken');
|
||||
@@ -191,4 +191,161 @@ describe('Auth Middleware', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('requireVerifiedEmail Middleware', () => {
|
||||
let req, res, next;
|
||||
|
||||
beforeEach(() => {
|
||||
req = {
|
||||
user: null
|
||||
};
|
||||
res = {
|
||||
status: jest.fn().mockReturnThis(),
|
||||
json: jest.fn()
|
||||
};
|
||||
next = jest.fn();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Verified users', () => {
|
||||
it('should call next for verified user', () => {
|
||||
req.user = {
|
||||
id: 1,
|
||||
email: 'verified@test.com',
|
||||
isVerified: true
|
||||
};
|
||||
|
||||
requireVerifiedEmail(req, res, next);
|
||||
|
||||
expect(next).toHaveBeenCalled();
|
||||
expect(res.status).not.toHaveBeenCalled();
|
||||
expect(res.json).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call next for verified OAuth user', () => {
|
||||
req.user = {
|
||||
id: 2,
|
||||
email: 'google@test.com',
|
||||
authProvider: 'google',
|
||||
isVerified: true
|
||||
};
|
||||
|
||||
requireVerifiedEmail(req, res, next);
|
||||
|
||||
expect(next).toHaveBeenCalled();
|
||||
expect(res.status).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Unverified users', () => {
|
||||
it('should return 403 for unverified user', () => {
|
||||
req.user = {
|
||||
id: 1,
|
||||
email: 'unverified@test.com',
|
||||
isVerified: false
|
||||
};
|
||||
|
||||
requireVerifiedEmail(req, res, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(403);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
error: 'Email verification required. Please verify your email address to perform this action.',
|
||||
code: 'EMAIL_NOT_VERIFIED'
|
||||
});
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return 403 when isVerified is null', () => {
|
||||
req.user = {
|
||||
id: 1,
|
||||
email: 'test@test.com',
|
||||
isVerified: null
|
||||
};
|
||||
|
||||
requireVerifiedEmail(req, res, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(403);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
error: 'Email verification required. Please verify your email address to perform this action.',
|
||||
code: 'EMAIL_NOT_VERIFIED'
|
||||
});
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return 403 when isVerified is undefined', () => {
|
||||
req.user = {
|
||||
id: 1,
|
||||
email: 'test@test.com'
|
||||
// isVerified is undefined
|
||||
};
|
||||
|
||||
requireVerifiedEmail(req, res, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(403);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
error: 'Email verification required. Please verify your email address to perform this action.',
|
||||
code: 'EMAIL_NOT_VERIFIED'
|
||||
});
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('No user', () => {
|
||||
it('should return 401 when user is not set', () => {
|
||||
req.user = null;
|
||||
|
||||
requireVerifiedEmail(req, res, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(401);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
error: 'Authentication required',
|
||||
code: 'NO_AUTH'
|
||||
});
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return 401 when user is undefined', () => {
|
||||
req.user = undefined;
|
||||
|
||||
requireVerifiedEmail(req, res, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(401);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
error: 'Authentication required',
|
||||
code: 'NO_AUTH'
|
||||
});
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases', () => {
|
||||
it('should handle user object with extra fields', () => {
|
||||
req.user = {
|
||||
id: 1,
|
||||
email: 'test@test.com',
|
||||
isVerified: true,
|
||||
firstName: 'Test',
|
||||
lastName: 'User',
|
||||
phone: '1234567890'
|
||||
};
|
||||
|
||||
requireVerifiedEmail(req, res, next);
|
||||
|
||||
expect(next).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should prioritize missing user over unverified user', () => {
|
||||
// If called without authenticateToken first
|
||||
req.user = null;
|
||||
|
||||
requireVerifiedEmail(req, res, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(401);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
error: 'Authentication required',
|
||||
code: 'NO_AUTH'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -38,11 +38,17 @@ jest.mock('../../../middleware/rateLimiter', () => ({
|
||||
registerLimiter: (req, res, next) => next(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../services/emailService', () => ({
|
||||
sendVerificationEmail: jest.fn()
|
||||
}));
|
||||
|
||||
const { User } = require('../../../models');
|
||||
const emailService = require('../../../services/emailService');
|
||||
|
||||
// Set up OAuth2Client mock before requiring authRoutes
|
||||
const mockGoogleClient = {
|
||||
verifyIdToken: jest.fn()
|
||||
verifyIdToken: jest.fn(),
|
||||
getToken: jest.fn()
|
||||
};
|
||||
OAuth2Client.mockImplementation(() => mockGoogleClient);
|
||||
|
||||
@@ -90,10 +96,13 @@ describe('Auth Routes', () => {
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
firstName: 'Test',
|
||||
lastName: 'User'
|
||||
lastName: 'User',
|
||||
isVerified: false,
|
||||
generateVerificationToken: jest.fn().mockResolvedValue()
|
||||
};
|
||||
|
||||
User.create.mockResolvedValue(newUser);
|
||||
emailService.sendVerificationEmail.mockResolvedValue();
|
||||
|
||||
const response = await request(app)
|
||||
.post('/auth/register')
|
||||
@@ -112,8 +121,12 @@ describe('Auth Routes', () => {
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
firstName: 'Test',
|
||||
lastName: 'User'
|
||||
lastName: 'User',
|
||||
isVerified: false
|
||||
});
|
||||
expect(response.body.verificationEmailSent).toBe(true);
|
||||
expect(newUser.generateVerificationToken).toHaveBeenCalled();
|
||||
expect(emailService.sendVerificationEmail).toHaveBeenCalledWith(newUser, newUser.verificationToken);
|
||||
|
||||
// Check that cookies are set
|
||||
expect(response.headers['set-cookie']).toEqual(
|
||||
@@ -571,8 +584,17 @@ describe('Auth Routes', () => {
|
||||
process.env.NODE_ENV = 'prod';
|
||||
|
||||
User.findOne.mockResolvedValue(null);
|
||||
const newUser = { id: 1, username: 'test', email: 'test@example.com', firstName: 'Test', lastName: 'User' };
|
||||
const newUser = {
|
||||
id: 1,
|
||||
username: 'test',
|
||||
email: 'test@example.com',
|
||||
firstName: 'Test',
|
||||
lastName: 'User',
|
||||
isVerified: false,
|
||||
generateVerificationToken: jest.fn().mockResolvedValue()
|
||||
};
|
||||
User.create.mockResolvedValue(newUser);
|
||||
emailService.sendVerificationEmail.mockResolvedValue();
|
||||
jwt.sign.mockReturnValueOnce('access').mockReturnValueOnce('refresh');
|
||||
|
||||
const response = await request(app)
|
||||
@@ -629,7 +651,17 @@ describe('Auth Routes', () => {
|
||||
describe('Token management', () => {
|
||||
it('should generate both access and refresh tokens on registration', async () => {
|
||||
User.findOne.mockResolvedValue(null);
|
||||
User.create.mockResolvedValue({ id: 1, username: 'test', email: 'test@example.com', firstName: 'Test', lastName: 'User' });
|
||||
const newUser = {
|
||||
id: 1,
|
||||
username: 'test',
|
||||
email: 'test@example.com',
|
||||
firstName: 'Test',
|
||||
lastName: 'User',
|
||||
isVerified: false,
|
||||
generateVerificationToken: jest.fn().mockResolvedValue()
|
||||
};
|
||||
User.create.mockResolvedValue(newUser);
|
||||
emailService.sendVerificationEmail.mockResolvedValue();
|
||||
|
||||
jwt.sign
|
||||
.mockReturnValueOnce('access-token')
|
||||
@@ -659,7 +691,17 @@ describe('Auth Routes', () => {
|
||||
|
||||
it('should set correct cookie options', async () => {
|
||||
User.findOne.mockResolvedValue(null);
|
||||
User.create.mockResolvedValue({ id: 1, username: 'test', email: 'test@example.com', firstName: 'Test', lastName: 'User' });
|
||||
const newUser = {
|
||||
id: 1,
|
||||
username: 'test',
|
||||
email: 'test@example.com',
|
||||
firstName: 'Test',
|
||||
lastName: 'User',
|
||||
isVerified: false,
|
||||
generateVerificationToken: jest.fn().mockResolvedValue()
|
||||
};
|
||||
User.create.mockResolvedValue(newUser);
|
||||
emailService.sendVerificationEmail.mockResolvedValue();
|
||||
jwt.sign.mockReturnValueOnce('access').mockReturnValueOnce('refresh');
|
||||
|
||||
const response = await request(app)
|
||||
@@ -679,4 +721,304 @@ describe('Auth Routes', () => {
|
||||
expect(cookies[1]).toContain('SameSite=Strict');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Email Verification', () => {
|
||||
describe('Registration with verification', () => {
|
||||
it('should continue registration even if verification email fails', async () => {
|
||||
User.findOne.mockResolvedValue(null);
|
||||
|
||||
const newUser = {
|
||||
id: 1,
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
firstName: 'Test',
|
||||
lastName: 'User',
|
||||
isVerified: false,
|
||||
generateVerificationToken: jest.fn().mockResolvedValue()
|
||||
};
|
||||
|
||||
User.create.mockResolvedValue(newUser);
|
||||
emailService.sendVerificationEmail.mockRejectedValue(new Error('Email service down'));
|
||||
|
||||
const response = await request(app)
|
||||
.post('/auth/register')
|
||||
.send({
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
password: 'StrongPass123!',
|
||||
firstName: 'Test',
|
||||
lastName: 'User'
|
||||
});
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body.user.id).toBe(1);
|
||||
expect(response.body.verificationEmailSent).toBe(false);
|
||||
expect(newUser.generateVerificationToken).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Google OAuth auto-verification', () => {
|
||||
it('should auto-verify Google OAuth users', async () => {
|
||||
const mockPayload = {
|
||||
sub: 'google456',
|
||||
email: 'oauth@gmail.com',
|
||||
given_name: 'OAuth',
|
||||
family_name: 'User',
|
||||
picture: 'pic.jpg'
|
||||
};
|
||||
|
||||
mockGoogleClient.getToken.mockResolvedValue({
|
||||
tokens: { id_token: 'google-id-token' }
|
||||
});
|
||||
|
||||
mockGoogleClient.verifyIdToken.mockResolvedValue({
|
||||
getPayload: () => mockPayload
|
||||
});
|
||||
|
||||
User.findOne
|
||||
.mockResolvedValueOnce(null) // No Google user
|
||||
.mockResolvedValueOnce(null); // No email user
|
||||
|
||||
const createdUser = {
|
||||
id: 1,
|
||||
username: 'oauth_gle456',
|
||||
email: 'oauth@gmail.com',
|
||||
firstName: 'OAuth',
|
||||
lastName: 'User',
|
||||
profileImage: 'pic.jpg',
|
||||
isVerified: true
|
||||
};
|
||||
|
||||
User.create.mockResolvedValue(createdUser);
|
||||
jwt.sign.mockReturnValueOnce('access-token').mockReturnValueOnce('refresh-token');
|
||||
|
||||
const response = await request(app)
|
||||
.post('/auth/google')
|
||||
.send({
|
||||
code: 'google-auth-code'
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(User.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
isVerified: true,
|
||||
verifiedAt: expect.any(Date)
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /auth/verify-email', () => {
|
||||
it('should verify email with valid token', async () => {
|
||||
const mockUser = {
|
||||
id: 1,
|
||||
email: 'test@example.com',
|
||||
verificationToken: 'valid-token-123',
|
||||
verificationTokenExpiry: new Date(Date.now() + 60 * 60 * 1000),
|
||||
isVerified: false,
|
||||
isVerificationTokenValid: function(token) {
|
||||
return this.verificationToken === token &&
|
||||
new Date() < new Date(this.verificationTokenExpiry);
|
||||
},
|
||||
verifyEmail: jest.fn().mockResolvedValue({
|
||||
id: 1,
|
||||
email: 'test@example.com',
|
||||
isVerified: true
|
||||
})
|
||||
};
|
||||
|
||||
User.findOne.mockResolvedValue(mockUser);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/auth/verify-email')
|
||||
.send({ token: 'valid-token-123' });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.message).toBe('Email verified successfully');
|
||||
expect(response.body.user).toEqual({
|
||||
id: 1,
|
||||
email: 'test@example.com',
|
||||
isVerified: true
|
||||
});
|
||||
expect(mockUser.verifyEmail).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reject missing token', async () => {
|
||||
const response = await request(app)
|
||||
.post('/auth/verify-email')
|
||||
.send({});
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toBe('Verification token required');
|
||||
expect(response.body.code).toBe('TOKEN_REQUIRED');
|
||||
});
|
||||
|
||||
it('should reject invalid token', async () => {
|
||||
User.findOne.mockResolvedValue(null);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/auth/verify-email')
|
||||
.send({ token: 'invalid-token' });
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toBe('Invalid verification token');
|
||||
expect(response.body.code).toBe('VERIFICATION_TOKEN_INVALID');
|
||||
});
|
||||
|
||||
it('should reject already verified user', async () => {
|
||||
const mockUser = {
|
||||
id: 1,
|
||||
email: 'verified@example.com',
|
||||
isVerified: true
|
||||
};
|
||||
|
||||
User.findOne.mockResolvedValue(mockUser);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/auth/verify-email')
|
||||
.send({ token: 'some-token' });
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toBe('Email already verified');
|
||||
expect(response.body.code).toBe('ALREADY_VERIFIED');
|
||||
});
|
||||
|
||||
it('should reject expired token', async () => {
|
||||
const mockUser = {
|
||||
id: 1,
|
||||
email: 'test@example.com',
|
||||
verificationToken: 'expired-token',
|
||||
verificationTokenExpiry: new Date(Date.now() - 60 * 60 * 1000),
|
||||
isVerified: false,
|
||||
isVerificationTokenValid: jest.fn().mockReturnValue(false)
|
||||
};
|
||||
|
||||
User.findOne.mockResolvedValue(mockUser);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/auth/verify-email')
|
||||
.send({ token: 'expired-token' });
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toBe('Verification token has expired. Please request a new one.');
|
||||
expect(response.body.code).toBe('VERIFICATION_TOKEN_EXPIRED');
|
||||
});
|
||||
|
||||
it('should handle verification errors', async () => {
|
||||
User.findOne.mockRejectedValue(new Error('Database error'));
|
||||
|
||||
const response = await request(app)
|
||||
.post('/auth/verify-email')
|
||||
.send({ token: 'valid-token' });
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body.error).toBe('Email verification failed. Please try again.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /auth/resend-verification', () => {
|
||||
it('should resend verification email for authenticated unverified user', async () => {
|
||||
const mockUser = {
|
||||
id: 1,
|
||||
email: 'test@example.com',
|
||||
firstName: 'Test',
|
||||
isVerified: false,
|
||||
verificationToken: 'new-token',
|
||||
generateVerificationToken: jest.fn().mockResolvedValue()
|
||||
};
|
||||
|
||||
jwt.verify.mockReturnValue({ id: 1 });
|
||||
User.findByPk.mockResolvedValue(mockUser);
|
||||
emailService.sendVerificationEmail.mockResolvedValue();
|
||||
|
||||
const response = await request(app)
|
||||
.post('/auth/resend-verification')
|
||||
.set('Cookie', ['accessToken=valid-token']);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.message).toBe('Verification email sent successfully');
|
||||
expect(mockUser.generateVerificationToken).toHaveBeenCalled();
|
||||
expect(emailService.sendVerificationEmail).toHaveBeenCalledWith(mockUser, mockUser.verificationToken);
|
||||
});
|
||||
|
||||
it('should reject when no access token provided', async () => {
|
||||
const response = await request(app)
|
||||
.post('/auth/resend-verification');
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(response.body.error).toBe('Authentication required');
|
||||
expect(response.body.code).toBe('NO_TOKEN');
|
||||
});
|
||||
|
||||
it('should reject expired access token', async () => {
|
||||
const error = new Error('jwt expired');
|
||||
error.name = 'TokenExpiredError';
|
||||
jwt.verify.mockImplementation(() => {
|
||||
throw error;
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.post('/auth/resend-verification')
|
||||
.set('Cookie', ['accessToken=expired-token']);
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(response.body.error).toBe('Session expired. Please log in again.');
|
||||
expect(response.body.code).toBe('TOKEN_EXPIRED');
|
||||
});
|
||||
|
||||
it('should reject when user not found', async () => {
|
||||
jwt.verify.mockReturnValue({ id: 999 });
|
||||
User.findByPk.mockResolvedValue(null);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/auth/resend-verification')
|
||||
.set('Cookie', ['accessToken=valid-token']);
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body.error).toBe('User not found');
|
||||
expect(response.body.code).toBe('USER_NOT_FOUND');
|
||||
});
|
||||
|
||||
it('should reject when user already verified', async () => {
|
||||
const mockUser = {
|
||||
id: 1,
|
||||
email: 'verified@example.com',
|
||||
isVerified: true
|
||||
};
|
||||
|
||||
jwt.verify.mockReturnValue({ id: 1 });
|
||||
User.findByPk.mockResolvedValue(mockUser);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/auth/resend-verification')
|
||||
.set('Cookie', ['accessToken=valid-token']);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toBe('Email already verified');
|
||||
expect(response.body.code).toBe('ALREADY_VERIFIED');
|
||||
});
|
||||
|
||||
it('should handle email service failure', async () => {
|
||||
const mockUser = {
|
||||
id: 1,
|
||||
email: 'test@example.com',
|
||||
isVerified: false,
|
||||
verificationToken: 'new-token',
|
||||
generateVerificationToken: jest.fn().mockResolvedValue()
|
||||
};
|
||||
|
||||
jwt.verify.mockReturnValue({ id: 1 });
|
||||
User.findByPk.mockResolvedValue(mockUser);
|
||||
emailService.sendVerificationEmail.mockRejectedValue(new Error('Email service down'));
|
||||
|
||||
const response = await request(app)
|
||||
.post('/auth/resend-verification')
|
||||
.set('Cookie', ['accessToken=valid-token']);
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body.error).toBe('Failed to send verification email. Please try again.');
|
||||
expect(mockUser.generateVerificationToken).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user