add changes
This commit is contained in:
316
__tests__/auth.test.js
Normal file
316
__tests__/auth.test.js
Normal file
@@ -0,0 +1,316 @@
|
||||
const request = require('supertest');
|
||||
const express = require('express');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const authRoutes = require('../routes/auth.routes');
|
||||
const { User, GuestSession, QuizSession, sequelize } = require('../models');
|
||||
|
||||
// Create Express app for testing
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use('/api/auth', authRoutes);
|
||||
|
||||
describe('Authentication Endpoints', () => {
|
||||
let testUser;
|
||||
let authToken;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Sync database
|
||||
await sequelize.sync({ force: true });
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// Clean up
|
||||
await User.destroy({ where: {}, force: true });
|
||||
await sequelize.close();
|
||||
});
|
||||
|
||||
describe('POST /api/auth/register', () => {
|
||||
it('should register a new user successfully', async () => {
|
||||
const userData = {
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
password: 'Test@123'
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/auth/register')
|
||||
.send(userData)
|
||||
.expect(201);
|
||||
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(response.body.message).toBe('User registered successfully');
|
||||
expect(response.body.data).toHaveProperty('user');
|
||||
expect(response.body.data).toHaveProperty('token');
|
||||
expect(response.body.data.user.email).toBe(userData.email);
|
||||
expect(response.body.data.user.username).toBe(userData.username);
|
||||
expect(response.body.data.user).not.toHaveProperty('password');
|
||||
|
||||
testUser = response.body.data.user;
|
||||
authToken = response.body.data.token;
|
||||
});
|
||||
|
||||
it('should reject registration with duplicate email', async () => {
|
||||
const userData = {
|
||||
username: 'anotheruser',
|
||||
email: 'test@example.com', // Same email
|
||||
password: 'Test@123'
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/auth/register')
|
||||
.send(userData)
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.success).toBe(false);
|
||||
expect(response.body.message).toBe('Email already registered');
|
||||
});
|
||||
|
||||
it('should reject registration with duplicate username', async () => {
|
||||
const userData = {
|
||||
username: 'testuser', // Same username
|
||||
email: 'another@example.com',
|
||||
password: 'Test@123'
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/auth/register')
|
||||
.send(userData)
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.success).toBe(false);
|
||||
expect(response.body.message).toBe('Username already taken');
|
||||
});
|
||||
|
||||
it('should reject registration with invalid email', async () => {
|
||||
const userData = {
|
||||
username: 'newuser',
|
||||
email: 'invalid-email',
|
||||
password: 'Test@123'
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/auth/register')
|
||||
.send(userData)
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.success).toBe(false);
|
||||
expect(response.body.message).toBe('Validation failed');
|
||||
});
|
||||
|
||||
it('should reject registration with weak password', async () => {
|
||||
const userData = {
|
||||
username: 'newuser',
|
||||
email: 'new@example.com',
|
||||
password: 'weak'
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/auth/register')
|
||||
.send(userData)
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.success).toBe(false);
|
||||
expect(response.body.message).toBe('Validation failed');
|
||||
});
|
||||
|
||||
it('should reject registration with username too short', async () => {
|
||||
const userData = {
|
||||
username: 'ab', // Only 2 characters
|
||||
email: 'new@example.com',
|
||||
password: 'Test@123'
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/auth/register')
|
||||
.send(userData)
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.success).toBe(false);
|
||||
expect(response.body.message).toBe('Validation failed');
|
||||
});
|
||||
|
||||
it('should reject registration with invalid username characters', async () => {
|
||||
const userData = {
|
||||
username: 'test-user!', // Contains invalid characters
|
||||
email: 'new@example.com',
|
||||
password: 'Test@123'
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/auth/register')
|
||||
.send(userData)
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.success).toBe(false);
|
||||
expect(response.body.message).toBe('Validation failed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /api/auth/register with guest migration', () => {
|
||||
let guestSession;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Create a guest session with quiz data
|
||||
guestSession = await GuestSession.create({
|
||||
id: uuidv4(),
|
||||
guest_id: `guest_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
session_token: 'test-guest-token',
|
||||
expires_at: new Date(Date.now() + 24 * 60 * 60 * 1000),
|
||||
max_quizzes: 3,
|
||||
quizzes_attempted: 2,
|
||||
is_converted: false
|
||||
});
|
||||
|
||||
// Create quiz sessions for the guest
|
||||
await QuizSession.create({
|
||||
id: uuidv4(),
|
||||
guest_session_id: guestSession.id,
|
||||
category_id: uuidv4(),
|
||||
quiz_type: 'practice',
|
||||
difficulty: 'easy',
|
||||
status: 'completed',
|
||||
questions_count: 5,
|
||||
questions_answered: 5,
|
||||
correct_answers: 4,
|
||||
score: 40,
|
||||
percentage: 80,
|
||||
is_passed: true,
|
||||
started_at: new Date(),
|
||||
completed_at: new Date()
|
||||
});
|
||||
});
|
||||
|
||||
it('should register user and migrate guest data', async () => {
|
||||
const userData = {
|
||||
username: 'guestconvert',
|
||||
email: 'guestconvert@example.com',
|
||||
password: 'Test@123',
|
||||
guestSessionId: guestSession.guest_id
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/auth/register')
|
||||
.send(userData)
|
||||
.expect(201);
|
||||
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(response.body.data).toHaveProperty('migratedData');
|
||||
expect(response.body.data.migratedData).toHaveProperty('quizzes');
|
||||
expect(response.body.data.migratedData).toHaveProperty('stats');
|
||||
|
||||
// Verify guest session is marked as converted
|
||||
await guestSession.reload();
|
||||
expect(guestSession.is_converted).toBe(true);
|
||||
expect(guestSession.converted_user_id).toBe(response.body.data.user.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /api/auth/login', () => {
|
||||
it('should login with valid credentials', async () => {
|
||||
const credentials = {
|
||||
email: 'test@example.com',
|
||||
password: 'Test@123'
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/auth/login')
|
||||
.send(credentials)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(response.body.message).toBe('Login successful');
|
||||
expect(response.body.data).toHaveProperty('user');
|
||||
expect(response.body.data).toHaveProperty('token');
|
||||
expect(response.body.data.user).not.toHaveProperty('password');
|
||||
});
|
||||
|
||||
it('should reject login with invalid email', async () => {
|
||||
const credentials = {
|
||||
email: 'nonexistent@example.com',
|
||||
password: 'Test@123'
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/auth/login')
|
||||
.send(credentials)
|
||||
.expect(401);
|
||||
|
||||
expect(response.body.success).toBe(false);
|
||||
expect(response.body.message).toBe('Invalid email or password');
|
||||
});
|
||||
|
||||
it('should reject login with invalid password', async () => {
|
||||
const credentials = {
|
||||
email: 'test@example.com',
|
||||
password: 'WrongPassword123'
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/auth/login')
|
||||
.send(credentials)
|
||||
.expect(401);
|
||||
|
||||
expect(response.body.success).toBe(false);
|
||||
expect(response.body.message).toBe('Invalid email or password');
|
||||
});
|
||||
|
||||
it('should reject login with missing fields', async () => {
|
||||
const credentials = {
|
||||
email: 'test@example.com'
|
||||
// Missing password
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/auth/login')
|
||||
.send(credentials)
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.success).toBe(false);
|
||||
expect(response.body.message).toBe('Validation failed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/auth/verify', () => {
|
||||
it('should verify valid token', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/auth/verify')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(response.body.message).toBe('Token valid');
|
||||
expect(response.body.data).toHaveProperty('user');
|
||||
expect(response.body.data.user.email).toBe('test@example.com');
|
||||
});
|
||||
|
||||
it('should reject request without token', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/auth/verify')
|
||||
.expect(401);
|
||||
|
||||
expect(response.body.success).toBe(false);
|
||||
expect(response.body.message).toContain('No token provided');
|
||||
});
|
||||
|
||||
it('should reject request with invalid token', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/auth/verify')
|
||||
.set('Authorization', 'Bearer invalid-token')
|
||||
.expect(401);
|
||||
|
||||
expect(response.body.success).toBe(false);
|
||||
expect(response.body.message).toContain('Invalid token');
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /api/auth/logout', () => {
|
||||
it('should logout successfully', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/auth/logout')
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(response.body.message).toContain('Logout successful');
|
||||
});
|
||||
});
|
||||
});
|
||||
354
__tests__/logout-verify.test.js
Normal file
354
__tests__/logout-verify.test.js
Normal file
@@ -0,0 +1,354 @@
|
||||
/**
|
||||
* Tests for Logout and Token Verification Endpoints
|
||||
* Task 14: User Logout & Token Verification
|
||||
*/
|
||||
|
||||
const request = require('supertest');
|
||||
const app = require('../server');
|
||||
const { User } = require('../models');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const config = require('../config/config');
|
||||
|
||||
describe('POST /api/auth/logout', () => {
|
||||
test('Should logout successfully (stateless JWT approach)', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/auth/logout')
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(response.body.message).toContain('Logout successful');
|
||||
});
|
||||
|
||||
test('Should return success even without token (stateless approach)', async () => {
|
||||
// In a stateless JWT system, logout is client-side only
|
||||
const response = await request(app)
|
||||
.post('/api/auth/logout')
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/auth/verify', () => {
|
||||
let testUser;
|
||||
let validToken;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Create a test user
|
||||
testUser = await User.create({
|
||||
username: 'verifyuser',
|
||||
email: 'verify@test.com',
|
||||
password: 'Test@123',
|
||||
role: 'user'
|
||||
});
|
||||
|
||||
// Generate valid token
|
||||
validToken = jwt.sign(
|
||||
{
|
||||
userId: testUser.id,
|
||||
email: testUser.email,
|
||||
username: testUser.username,
|
||||
role: testUser.role
|
||||
},
|
||||
config.jwt.secret,
|
||||
{ expiresIn: config.jwt.expire }
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// Cleanup
|
||||
if (testUser) {
|
||||
await testUser.destroy({ force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('Should verify valid token and return user info', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/auth/verify')
|
||||
.set('Authorization', `Bearer ${validToken}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(response.body.message).toBe('Token valid');
|
||||
expect(response.body.data.user).toBeDefined();
|
||||
expect(response.body.data.user.id).toBe(testUser.id);
|
||||
expect(response.body.data.user.email).toBe(testUser.email);
|
||||
expect(response.body.data.user.username).toBe(testUser.username);
|
||||
// Password should not be included
|
||||
expect(response.body.data.user.password).toBeUndefined();
|
||||
});
|
||||
|
||||
test('Should reject request without token', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/auth/verify')
|
||||
.expect(401);
|
||||
|
||||
expect(response.body.success).toBe(false);
|
||||
expect(response.body.message).toContain('No token provided');
|
||||
});
|
||||
|
||||
test('Should reject invalid token', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/auth/verify')
|
||||
.set('Authorization', 'Bearer invalid_token_here')
|
||||
.expect(401);
|
||||
|
||||
expect(response.body.success).toBe(false);
|
||||
expect(response.body.message).toContain('Invalid token');
|
||||
});
|
||||
|
||||
test('Should reject expired token', async () => {
|
||||
// Create an expired token
|
||||
const expiredToken = jwt.sign(
|
||||
{
|
||||
userId: testUser.id,
|
||||
email: testUser.email,
|
||||
username: testUser.username,
|
||||
role: testUser.role
|
||||
},
|
||||
config.jwt.secret,
|
||||
{ expiresIn: '0s' } // Immediately expired
|
||||
);
|
||||
|
||||
// Wait a moment to ensure expiration
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/auth/verify')
|
||||
.set('Authorization', `Bearer ${expiredToken}`)
|
||||
.expect(401);
|
||||
|
||||
expect(response.body.success).toBe(false);
|
||||
expect(response.body.message).toContain('expired');
|
||||
});
|
||||
|
||||
test('Should reject token with invalid format (no Bearer prefix)', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/auth/verify')
|
||||
.set('Authorization', validToken) // Missing "Bearer " prefix
|
||||
.expect(401);
|
||||
|
||||
expect(response.body.success).toBe(false);
|
||||
});
|
||||
|
||||
test('Should reject token for inactive user', async () => {
|
||||
// Deactivate the user
|
||||
await testUser.update({ is_active: false });
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/auth/verify')
|
||||
.set('Authorization', `Bearer ${validToken}`)
|
||||
.expect(404);
|
||||
|
||||
expect(response.body.success).toBe(false);
|
||||
expect(response.body.message).toContain('not found or inactive');
|
||||
|
||||
// Reactivate for cleanup
|
||||
await testUser.update({ is_active: true });
|
||||
});
|
||||
|
||||
test('Should reject token for non-existent user', async () => {
|
||||
// Create token with non-existent user ID
|
||||
const fakeToken = jwt.sign(
|
||||
{
|
||||
userId: '00000000-0000-0000-0000-000000000000',
|
||||
email: 'fake@test.com',
|
||||
username: 'fakeuser',
|
||||
role: 'user'
|
||||
},
|
||||
config.jwt.secret,
|
||||
{ expiresIn: config.jwt.expire }
|
||||
);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/auth/verify')
|
||||
.set('Authorization', `Bearer ${fakeToken}`)
|
||||
.expect(404);
|
||||
|
||||
expect(response.body.success).toBe(false);
|
||||
expect(response.body.message).toContain('User not found');
|
||||
});
|
||||
|
||||
test('Should handle malformed Authorization header', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/auth/verify')
|
||||
.set('Authorization', 'InvalidFormat')
|
||||
.expect(401);
|
||||
|
||||
expect(response.body.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Token Verification Integration Tests', () => {
|
||||
let registeredUser;
|
||||
let userToken;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Register a new user
|
||||
const registerResponse = await request(app)
|
||||
.post('/api/auth/register')
|
||||
.send({
|
||||
username: `integrationuser_${Date.now()}`,
|
||||
email: `integration_${Date.now()}@test.com`,
|
||||
password: 'Test@123'
|
||||
})
|
||||
.expect(201);
|
||||
|
||||
registeredUser = registerResponse.body.data.user;
|
||||
userToken = registerResponse.body.data.token;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// Cleanup
|
||||
if (registeredUser && registeredUser.id) {
|
||||
const user = await User.findByPk(registeredUser.id);
|
||||
if (user) {
|
||||
await user.destroy({ force: true });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('Should verify token immediately after registration', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/auth/verify')
|
||||
.set('Authorization', `Bearer ${userToken}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(response.body.data.user.id).toBe(registeredUser.id);
|
||||
});
|
||||
|
||||
test('Should verify token after login', async () => {
|
||||
// Login with the registered user
|
||||
const loginResponse = await request(app)
|
||||
.post('/api/auth/login')
|
||||
.send({
|
||||
email: registeredUser.email,
|
||||
password: 'Test@123'
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const loginToken = loginResponse.body.data.token;
|
||||
|
||||
// Verify the login token
|
||||
const verifyResponse = await request(app)
|
||||
.get('/api/auth/verify')
|
||||
.set('Authorization', `Bearer ${loginToken}`)
|
||||
.expect(200);
|
||||
|
||||
expect(verifyResponse.body.success).toBe(true);
|
||||
expect(verifyResponse.body.data.user.id).toBe(registeredUser.id);
|
||||
});
|
||||
|
||||
test('Should complete full auth flow: register -> verify -> logout', async () => {
|
||||
// 1. Register
|
||||
const registerResponse = await request(app)
|
||||
.post('/api/auth/register')
|
||||
.send({
|
||||
username: `flowuser_${Date.now()}`,
|
||||
email: `flow_${Date.now()}@test.com`,
|
||||
password: 'Test@123'
|
||||
})
|
||||
.expect(201);
|
||||
|
||||
const token = registerResponse.body.data.token;
|
||||
const userId = registerResponse.body.data.user.id;
|
||||
|
||||
// 2. Verify token
|
||||
const verifyResponse = await request(app)
|
||||
.get('/api/auth/verify')
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.expect(200);
|
||||
|
||||
expect(verifyResponse.body.success).toBe(true);
|
||||
|
||||
// 3. Logout
|
||||
const logoutResponse = await request(app)
|
||||
.post('/api/auth/logout')
|
||||
.expect(200);
|
||||
|
||||
expect(logoutResponse.body.success).toBe(true);
|
||||
|
||||
// 4. Token should still be valid (stateless JWT)
|
||||
// In a real app, client would delete the token
|
||||
const verifyAfterLogout = await request(app)
|
||||
.get('/api/auth/verify')
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.expect(200);
|
||||
|
||||
expect(verifyAfterLogout.body.success).toBe(true);
|
||||
|
||||
// Cleanup
|
||||
const user = await User.findByPk(userId);
|
||||
if (user) {
|
||||
await user.destroy({ force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Token Security Tests', () => {
|
||||
test('Should reject token signed with wrong secret', async () => {
|
||||
const fakeToken = jwt.sign(
|
||||
{
|
||||
userId: '12345',
|
||||
email: 'fake@test.com',
|
||||
username: 'fakeuser',
|
||||
role: 'user'
|
||||
},
|
||||
'wrong_secret_key',
|
||||
{ expiresIn: '24h' }
|
||||
);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/auth/verify')
|
||||
.set('Authorization', `Bearer ${fakeToken}`)
|
||||
.expect(401);
|
||||
|
||||
expect(response.body.success).toBe(false);
|
||||
expect(response.body.message).toContain('Invalid token');
|
||||
});
|
||||
|
||||
test('Should reject tampered token', async () => {
|
||||
// Create a valid token
|
||||
const validToken = jwt.sign(
|
||||
{
|
||||
userId: '12345',
|
||||
email: 'test@test.com',
|
||||
username: 'testuser',
|
||||
role: 'user'
|
||||
},
|
||||
config.jwt.secret,
|
||||
{ expiresIn: '24h' }
|
||||
);
|
||||
|
||||
// Tamper with the token by changing a character
|
||||
const tamperedToken = validToken.slice(0, -5) + 'XXXXX';
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/auth/verify')
|
||||
.set('Authorization', `Bearer ${tamperedToken}`)
|
||||
.expect(401);
|
||||
|
||||
expect(response.body.success).toBe(false);
|
||||
});
|
||||
|
||||
test('Should reject token with missing payload fields', async () => {
|
||||
// Create token with incomplete payload
|
||||
const incompleteToken = jwt.sign(
|
||||
{
|
||||
userId: '12345'
|
||||
// Missing email, username, role
|
||||
},
|
||||
config.jwt.secret,
|
||||
{ expiresIn: '24h' }
|
||||
);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/auth/verify')
|
||||
.set('Authorization', `Bearer ${incompleteToken}`)
|
||||
.expect(404);
|
||||
|
||||
// Token is valid but user doesn't exist
|
||||
expect(response.body.success).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user