298 lines
9.4 KiB
JavaScript
298 lines
9.4 KiB
JavaScript
const crypto = require('crypto');
|
|
|
|
// Mock crypto module
|
|
jest.mock('crypto');
|
|
|
|
// Mock the entire models module
|
|
jest.mock('../../../models', () => {
|
|
const mockUser = {
|
|
update: jest.fn(),
|
|
verificationToken: null,
|
|
verificationTokenExpiry: null,
|
|
isVerified: false,
|
|
verifiedAt: null
|
|
};
|
|
|
|
return {
|
|
User: mockUser,
|
|
sequelize: {
|
|
models: {
|
|
User: mockUser
|
|
}
|
|
}
|
|
};
|
|
});
|
|
|
|
// Import User model methods - we'll test them directly
|
|
const User = require('../../../models/User');
|
|
|
|
describe('User Model - Email Verification', () => {
|
|
let mockUser;
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
|
|
// Create a fresh mock user for each test
|
|
mockUser = {
|
|
id: 1,
|
|
email: 'test@example.com',
|
|
firstName: 'Test',
|
|
lastName: 'User',
|
|
verificationToken: null,
|
|
verificationTokenExpiry: null,
|
|
verificationAttempts: 0,
|
|
isVerified: false,
|
|
verifiedAt: null,
|
|
update: jest.fn().mockImplementation(function(updates) {
|
|
Object.assign(this, updates);
|
|
return Promise.resolve(this);
|
|
})
|
|
};
|
|
|
|
// Add the prototype methods to mockUser
|
|
Object.setPrototypeOf(mockUser, User.prototype);
|
|
});
|
|
|
|
describe('generateVerificationToken', () => {
|
|
it('should generate a 6-digit code and set 24-hour expiry', async () => {
|
|
const mockCode = 123456;
|
|
crypto.randomInt.mockReturnValue(mockCode);
|
|
|
|
await User.prototype.generateVerificationToken.call(mockUser);
|
|
|
|
expect(crypto.randomInt).toHaveBeenCalledWith(100000, 999999);
|
|
expect(mockUser.update).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
verificationToken: '123456',
|
|
verificationAttempts: 0,
|
|
})
|
|
);
|
|
|
|
// Check that expiry is approximately 24 hours from now
|
|
const updateCall = mockUser.update.mock.calls[0][0];
|
|
const expiryTime = updateCall.verificationTokenExpiry.getTime();
|
|
const expectedExpiry = Date.now() + 24 * 60 * 60 * 1000;
|
|
|
|
expect(expiryTime).toBeGreaterThan(expectedExpiry - 1000);
|
|
expect(expiryTime).toBeLessThan(expectedExpiry + 1000);
|
|
});
|
|
|
|
it('should update the user with code and expiry', async () => {
|
|
const mockCode = 654321;
|
|
crypto.randomInt.mockReturnValue(mockCode);
|
|
|
|
const result = await User.prototype.generateVerificationToken.call(mockUser);
|
|
|
|
expect(mockUser.update).toHaveBeenCalledTimes(1);
|
|
expect(result.verificationToken).toBe('654321');
|
|
expect(result.verificationTokenExpiry).toBeInstanceOf(Date);
|
|
});
|
|
|
|
it('should generate unique codes on multiple calls', async () => {
|
|
crypto.randomInt
|
|
.mockReturnValueOnce(111111)
|
|
.mockReturnValueOnce(222222);
|
|
|
|
await User.prototype.generateVerificationToken.call(mockUser);
|
|
const firstCode = mockUser.update.mock.calls[0][0].verificationToken;
|
|
|
|
await User.prototype.generateVerificationToken.call(mockUser);
|
|
const secondCode = mockUser.update.mock.calls[1][0].verificationToken;
|
|
|
|
expect(firstCode).not.toBe(secondCode);
|
|
});
|
|
});
|
|
|
|
describe('isVerificationTokenValid', () => {
|
|
beforeEach(() => {
|
|
// Mock timingSafeEqual to do a simple comparison
|
|
crypto.timingSafeEqual = jest.fn((a, b) => a.equals(b));
|
|
});
|
|
|
|
it('should return true for valid token and non-expired time', () => {
|
|
const validToken = '123456';
|
|
const futureExpiry = new Date(Date.now() + 60 * 60 * 1000); // 1 hour from now
|
|
|
|
mockUser.verificationToken = validToken;
|
|
mockUser.verificationTokenExpiry = futureExpiry;
|
|
|
|
const result = User.prototype.isVerificationTokenValid.call(mockUser, validToken);
|
|
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should return false for missing token', () => {
|
|
mockUser.verificationToken = null;
|
|
mockUser.verificationTokenExpiry = new Date(Date.now() + 60 * 60 * 1000);
|
|
|
|
const result = User.prototype.isVerificationTokenValid.call(mockUser, 'any-token');
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should return false for missing expiry', () => {
|
|
mockUser.verificationToken = '123456';
|
|
mockUser.verificationTokenExpiry = null;
|
|
|
|
const result = User.prototype.isVerificationTokenValid.call(mockUser, '123456');
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should return false for mismatched token', () => {
|
|
mockUser.verificationToken = '123456';
|
|
mockUser.verificationTokenExpiry = new Date(Date.now() + 60 * 60 * 1000);
|
|
|
|
const result = User.prototype.isVerificationTokenValid.call(mockUser, '654321');
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should return false for expired token', () => {
|
|
const validToken = '123456';
|
|
const pastExpiry = new Date(Date.now() - 60 * 60 * 1000); // 1 hour ago
|
|
|
|
mockUser.verificationToken = validToken;
|
|
mockUser.verificationTokenExpiry = pastExpiry;
|
|
|
|
const result = User.prototype.isVerificationTokenValid.call(mockUser, validToken);
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should return false for token expiring in the past by 1 second', () => {
|
|
const validToken = '123456';
|
|
const pastExpiry = new Date(Date.now() - 1000); // 1 second ago
|
|
|
|
mockUser.verificationToken = validToken;
|
|
mockUser.verificationTokenExpiry = pastExpiry;
|
|
|
|
const result = User.prototype.isVerificationTokenValid.call(mockUser, validToken);
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should handle edge case of token expiring exactly now', () => {
|
|
const validToken = '123456';
|
|
// Set expiry 1ms in the future to handle timing precision
|
|
const nowExpiry = new Date(Date.now() + 1);
|
|
|
|
mockUser.verificationToken = validToken;
|
|
mockUser.verificationTokenExpiry = nowExpiry;
|
|
|
|
// This should be true because expiry is slightly in the future
|
|
const result = User.prototype.isVerificationTokenValid.call(mockUser, validToken);
|
|
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should handle string dates correctly', () => {
|
|
const validToken = '123456';
|
|
const futureExpiry = new Date(Date.now() + 60 * 60 * 1000).toISOString(); // String date
|
|
|
|
mockUser.verificationToken = validToken;
|
|
mockUser.verificationTokenExpiry = futureExpiry;
|
|
|
|
const result = User.prototype.isVerificationTokenValid.call(mockUser, validToken);
|
|
|
|
expect(result).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('verifyEmail', () => {
|
|
it('should mark user as verified and clear token fields', async () => {
|
|
mockUser.verificationToken = '123456';
|
|
mockUser.verificationTokenExpiry = new Date();
|
|
|
|
await User.prototype.verifyEmail.call(mockUser);
|
|
|
|
expect(mockUser.update).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
isVerified: true,
|
|
verificationToken: null,
|
|
verificationTokenExpiry: null
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should set verifiedAt timestamp', async () => {
|
|
const beforeTime = Date.now();
|
|
|
|
await User.prototype.verifyEmail.call(mockUser);
|
|
|
|
const updateCall = mockUser.update.mock.calls[0][0];
|
|
const verifiedAtTime = updateCall.verifiedAt.getTime();
|
|
const afterTime = Date.now();
|
|
|
|
expect(verifiedAtTime).toBeGreaterThanOrEqual(beforeTime);
|
|
expect(verifiedAtTime).toBeLessThanOrEqual(afterTime);
|
|
});
|
|
|
|
it('should return updated user object', async () => {
|
|
const result = await User.prototype.verifyEmail.call(mockUser);
|
|
|
|
expect(result.isVerified).toBe(true);
|
|
expect(result.verificationToken).toBe(null);
|
|
expect(result.verificationTokenExpiry).toBe(null);
|
|
expect(result.verifiedAt).toBeInstanceOf(Date);
|
|
});
|
|
|
|
it('should call update only once', async () => {
|
|
await User.prototype.verifyEmail.call(mockUser);
|
|
|
|
expect(mockUser.update).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|
|
describe('Complete verification flow', () => {
|
|
beforeEach(() => {
|
|
crypto.timingSafeEqual = jest.fn((a, b) => a.equals(b));
|
|
});
|
|
|
|
it('should complete full verification flow successfully', async () => {
|
|
// Step 1: Generate verification code
|
|
const mockCode = 999888;
|
|
crypto.randomInt.mockReturnValue(mockCode);
|
|
|
|
await User.prototype.generateVerificationToken.call(mockUser);
|
|
|
|
expect(mockUser.verificationToken).toBe('999888');
|
|
expect(mockUser.verificationTokenExpiry).toBeInstanceOf(Date);
|
|
|
|
// Step 2: Validate code
|
|
const isValid = User.prototype.isVerificationTokenValid.call(mockUser, '999888');
|
|
expect(isValid).toBe(true);
|
|
|
|
// Step 3: Verify email
|
|
await User.prototype.verifyEmail.call(mockUser);
|
|
|
|
expect(mockUser.isVerified).toBe(true);
|
|
expect(mockUser.verificationToken).toBe(null);
|
|
expect(mockUser.verificationTokenExpiry).toBe(null);
|
|
expect(mockUser.verifiedAt).toBeInstanceOf(Date);
|
|
});
|
|
|
|
it('should fail verification with wrong token', async () => {
|
|
// Generate code
|
|
crypto.randomInt.mockReturnValue(123456);
|
|
|
|
await User.prototype.generateVerificationToken.call(mockUser);
|
|
|
|
// Try to validate with wrong code
|
|
const isValid = User.prototype.isVerificationTokenValid.call(mockUser, '654321');
|
|
|
|
expect(isValid).toBe(false);
|
|
});
|
|
|
|
it('should fail verification with expired token', async () => {
|
|
// Manually set an expired token
|
|
mockUser.verificationToken = '123456';
|
|
mockUser.verificationTokenExpiry = new Date(Date.now() - 25 * 60 * 60 * 1000); // 25 hours ago
|
|
|
|
const isValid = User.prototype.isVerificationTokenValid.call(mockUser, '123456');
|
|
|
|
expect(isValid).toBe(false);
|
|
});
|
|
});
|
|
});
|