Files
flyer-crawler.projectium.com/src/routes/auth.routes.test.ts
Torben Sorensen 7added99b8
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 4m30s
lootsa tests fixes
2025-12-05 19:56:49 -08:00

333 lines
12 KiB
TypeScript

// src/routes/auth.routes.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import supertest from 'supertest';
import express, { Request } from 'express';
import cookieParser from 'cookie-parser';
import * as bcrypt from 'bcrypt';
import authRouter from './auth.routes';
import * as userDb from '../services/db/user.db';
import * as adminDb from '../services/db/admin.db';
import { UserProfile } from '../types';
// 1. Mock the Service Layer directly.
// This decouples the route tests from the SQL implementation details.
vi.mock('../services/db/user.db', () => ({
findUserByEmail: vi.fn(),
createUser: vi.fn(),
saveRefreshToken: vi.fn(),
createPasswordResetToken: vi.fn(),
getValidResetTokens: vi.fn(),
updateUserPassword: vi.fn(),
deleteResetToken: vi.fn(),
findUserByRefreshToken: vi.fn(),
}));
vi.mock('../services/db/admin.db', () => ({
logActivity: vi.fn(),
}));
// Mock the logger to keep test output clean
vi.mock('../services/logger.server', () => ({
logger: {
info: vi.fn(),
debug: vi.fn(),
error: vi.fn(),
warn: vi.fn(),
},
}));
// Mock the email service to prevent actual emails from being sent during tests.
vi.mock('../services/emailService.server', () => ({
sendPasswordResetEmail: vi.fn(),
}));
// Mock the bcrypt library to control password comparison results.
vi.mock('bcrypt', async (importOriginal) => {
const actual = await importOriginal<typeof bcrypt>();
return { ...actual, compare: vi.fn() };
});
// Define a type for the custom passport callback to avoid `any`.
type PassportCallback = (error: Error | null, user: Express.User | false, info?: { message: string }) => void;
// Mock Passport middleware
vi.mock('./passport.routes', () => ({
default: {
authenticate: (strategy: string, options: Record<string, unknown>, callback: PassportCallback) => (req: Request, res: any, next: any) => {
// Logic to simulate passport authentication outcome based on test input
if (req.body.password === 'wrong_password') {
// Simulate incorrect credentials
return callback(null, false, { message: 'Incorrect email or password.' });
}
if (req.body.email === 'locked@test.com') {
// Simulate locked account
return callback(null, false, { message: 'Account is temporarily locked.' });
}
if (req.body.email === 'notfound@test.com') {
// Simulate user not found
return callback(null, false, { message: 'Login failed' });
}
// Default success case
const user = { user_id: 'user-123', email: req.body.email };
callback(null, user, undefined);
},
initialize: () => (req: any, res: any, next: any) => next(),
},
}));
// Create a minimal Express app to host our router
const app = express();
app.use(express.json({ strict: false }));
app.use(cookieParser()); // Add cookie-parser middleware to populate req.cookies
app.use('/api/auth', authRouter);
// Add error handler to catch and log 500s during tests
app.use((err: any, req: Request, res: any, next: any) => {
console.error('[TEST APP ERROR]', err);
res.status(500).json({ message: err.message });
});
describe('Auth Routes (/api/auth)', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('POST /register', () => {
const newUserEmail = 'newuser@test.com';
const strongPassword = 'a-Very-Strong-Password-123!';
it('should successfully register a new user with a strong password', async () => {
// Arrange:
const mockNewUser: UserProfile = {
user_id: 'new-user-id',
user: { user_id: 'new-user-id', email: newUserEmail },
role: 'user',
points: 0,
full_name: 'Test User',
avatar_url: null,
preferences: {}
};
vi.mocked(userDb.findUserByEmail).mockResolvedValue(undefined); // No existing user
vi.mocked(userDb.createUser).mockResolvedValue(mockNewUser);
vi.mocked(userDb.saveRefreshToken).mockResolvedValue(undefined);
vi.mocked(adminDb.logActivity).mockResolvedValue(undefined);
// Act
const response = await supertest(app)
.post('/api/auth/register')
.send({
email: newUserEmail,
password: strongPassword,
full_name: 'Test User',
});
// Assert
expect(response.status).toBe(201);
expect(response.body.message).toBe('User registered successfully!');
expect(response.body.user.email).toBe(newUserEmail);
expect(response.body.token).toBeTypeOf('string');
});
it('should reject registration with a weak password', async () => {
const weakPassword = 'password';
const response = await supertest(app)
.post('/api/auth/register')
.send({
email: 'anotheruser@test.com',
password: weakPassword,
});
expect(response.status).toBe(400);
expect(response.body.message).toContain('Password is too weak');
});
it('should reject registration if the email already exists', async () => {
// Arrange: Mock that the user exists
vi.mocked(userDb.findUserByEmail).mockResolvedValue({
user_id: 'existing',
email: newUserEmail,
password_hash: 'some_hash',
failed_login_attempts: 0,
last_failed_login: null,
});
// Act
const response = await supertest(app)
.post('/api/auth/register')
.send({ email: newUserEmail, password: strongPassword });
// Assert
expect(response.status).toBe(409); // 409 Conflict
expect(response.body.message).toBe('User with that email already exists.');
});
it('should reject registration if email or password are not provided', async () => {
const response = await supertest(app)
.post('/api/auth/register')
.send({ email: newUserEmail /* no password */ });
// Assert
expect(response.status).toBe(400);
expect(response.body.message).toBe('Email and password are required.');
});
});
describe('POST /login', () => {
it('should successfully log in a user and return a token and cookie', async () => {
// Arrange:
const loginCredentials = { email: 'test@test.com', password: 'password123' };
vi.mocked(userDb.saveRefreshToken).mockResolvedValue(undefined);
// Act
const response = await supertest(app)
.post('/api/auth/login')
.send(loginCredentials);
// Assert
expect(response.status).toBe(200);
expect(response.body.user).toEqual({ user_id: 'user-123', email: loginCredentials.email });
expect(response.body.token).toBeTypeOf('string');
expect(response.headers['set-cookie']).toBeDefined();
});
it('should reject login for incorrect credentials', async () => {
const response = await supertest(app)
.post('/api/auth/login')
.send({ email: 'test@test.com', password: 'wrong_password' });
expect(response.status).toBe(401);
expect(response.body.message).toBe('Incorrect email or password.');
});
it('should reject login for a locked account', async () => {
const response = await supertest(app)
.post('/api/auth/login')
.send({ email: 'locked@test.com', password: 'password123' });
expect(response.status).toBe(401);
expect(response.body.message).toBe('Account is temporarily locked.');
});
it('should return 401 if user is not found', async () => {
const response = await supertest(app)
.post('/api/auth/login')
.send({ email: 'notfound@test.com', password: 'password123' });
expect(response.status).toBe(401);
});
});
describe('POST /forgot-password', () => {
it('should send a reset link if the user exists', async () => {
// Arrange
vi.mocked(userDb.findUserByEmail).mockResolvedValue({
user_id: 'user-123',
email: 'test@test.com',
password_hash: 'some_hash',
failed_login_attempts: 0,
last_failed_login: null,
});
vi.mocked(userDb.createPasswordResetToken).mockResolvedValue(undefined);
// Act
const response = await supertest(app)
.post('/api/auth/forgot-password')
.send({ email: 'test@test.com' });
// Assert
expect(response.status).toBe(200);
expect(response.body.message).toContain('a password reset link has been sent');
// In test env, token is returned
expect(response.body.token).toBeTypeOf('string');
});
it('should return a generic success message even if the user does not exist', async () => {
// Arrange
vi.mocked(userDb.findUserByEmail).mockResolvedValue(undefined);
// Act
const response = await supertest(app)
.post('/api/auth/forgot-password')
.send({ email: 'nouser@test.com' });
// Assert
expect(response.status).toBe(200);
expect(response.body.message).toContain('a password reset link has been sent');
});
});
describe('POST /reset-password', () => {
it('should reset the password with a valid token and strong password', async () => {
// Arrange
const tokenRecord = { user_id: 'user-123', token_hash: 'hashed-token', expires_at: new Date(Date.now() + 3600000) };
vi.mocked(userDb.getValidResetTokens).mockResolvedValue([tokenRecord]);
vi.mocked(bcrypt.compare).mockResolvedValue(true as never); // Token matches
vi.mocked(userDb.updateUserPassword).mockResolvedValue(undefined);
vi.mocked(userDb.deleteResetToken).mockResolvedValue(undefined);
vi.mocked(adminDb.logActivity).mockResolvedValue(undefined);
// Act
const response = await supertest(app)
.post('/api/auth/reset-password')
.send({ token: 'valid-token', newPassword: 'a-Very-Strong-Password-789!' });
// Assert
expect(response.status).toBe(200);
expect(response.body.message).toBe('Password has been reset successfully.');
});
it('should reject with an invalid or expired token', async () => {
// Arrange
vi.mocked(userDb.getValidResetTokens).mockResolvedValue([]); // No valid tokens found
// Act
const response = await supertest(app)
.post('/api/auth/reset-password')
.send({ token: 'invalid-token', newPassword: 'password123' });
// Assert
expect(response.status).toBe(400);
expect(response.body.message).toBe('Invalid or expired password reset token.');
});
});
describe('POST /refresh-token', () => {
it('should issue a new access token with a valid refresh token cookie', async () => {
// Arrange
const mockUser = {
user_id: 'user-123',
email: 'test@test.com',
password_hash: 'some_hash',
failed_login_attempts: 0,
last_failed_login: null,
};
vi.mocked(userDb.findUserByRefreshToken).mockResolvedValue(mockUser);
// Act
const response = await supertest(app)
.post('/api/auth/refresh-token')
.set('Cookie', 'refreshToken=valid-refresh-token');
// Assert
expect(response.status).toBe(200);
expect(response.body.token).toBeTypeOf('string');
});
it('should return 401 if no refresh token cookie is provided', async () => {
const response = await supertest(app).post('/api/auth/refresh-token');
expect(response.status).toBe(401);
expect(response.body.message).toBe('Refresh token not found.');
});
it('should return 403 if refresh token is invalid', async () => {
vi.mocked(userDb.findUserByRefreshToken).mockResolvedValue(undefined);
const response = await supertest(app)
.post('/api/auth/refresh-token')
.set('Cookie', 'refreshToken=invalid-token');
expect(response.status).toBe(403);
});
});
});