add changes

This commit is contained in:
AD2025
2025-11-12 23:06:27 +02:00
parent c664d0a341
commit ec6534fcc2
42 changed files with 11854 additions and 299 deletions

View File

@@ -0,0 +1,337 @@
const authController = require('../controllers/auth.controller');
const { User } = require('../models');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
// Mock dependencies
jest.mock('../models');
jest.mock('bcrypt');
jest.mock('jsonwebtoken');
describe('Auth Controller', () => {
let req, res;
beforeEach(() => {
req = {
body: {},
user: null
};
res = {
status: jest.fn().mockReturnThis(),
json: jest.fn().mockReturnThis()
};
jest.clearAllMocks();
});
describe('register', () => {
it('should register a new user successfully', async () => {
req.body = {
username: 'testuser',
email: 'test@example.com',
password: 'Test123!@#'
};
User.findOne = jest.fn().mockResolvedValue(null);
User.create = jest.fn().mockResolvedValue({
id: '123',
username: 'testuser',
email: 'test@example.com',
role: 'user'
});
jwt.sign = jest.fn().mockReturnValue('mock-token');
await authController.register(req, res);
expect(res.status).toHaveBeenCalledWith(201);
expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({
success: true,
message: 'User registered successfully',
data: expect.objectContaining({
token: 'mock-token',
user: expect.objectContaining({
username: 'testuser',
email: 'test@example.com'
})
})
})
);
});
it('should return 400 if username already exists', async () => {
req.body = {
username: 'existinguser',
email: 'new@example.com',
password: 'Test123!@#'
};
User.findOne = jest.fn().mockResolvedValue({ username: 'existinguser' });
await authController.register(req, res);
expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({
success: false,
message: 'Username already exists'
})
);
});
it('should return 409 if email already exists', async () => {
req.body = {
username: 'newuser',
email: 'existing@example.com',
password: 'Test123!@#'
};
User.findOne = jest.fn()
.mockResolvedValueOnce(null) // username check
.mockResolvedValueOnce({ email: 'existing@example.com' }); // email check
await authController.register(req, res);
expect(res.status).toHaveBeenCalledWith(409);
expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({
success: false,
message: 'Email already registered'
})
);
});
it('should return 400 for missing required fields', async () => {
req.body = {
username: 'testuser'
// missing email and password
};
await authController.register(req, res);
expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({
success: false
})
);
});
it('should return 500 on database error', async () => {
req.body = {
username: 'testuser',
email: 'test@example.com',
password: 'Test123!@#'
};
User.findOne = jest.fn().mockRejectedValue(new Error('Database error'));
await authController.register(req, res);
expect(res.status).toHaveBeenCalledWith(500);
expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({
success: false,
message: 'Internal server error'
})
);
});
});
describe('login', () => {
it('should login user successfully with email', async () => {
req.body = {
email: 'test@example.com',
password: 'Test123!@#'
};
const mockUser = {
id: '123',
username: 'testuser',
email: 'test@example.com',
password: 'hashed-password',
role: 'user',
isActive: true,
save: jest.fn()
};
User.findOne = jest.fn().mockResolvedValue(mockUser);
bcrypt.compare = jest.fn().mockResolvedValue(true);
jwt.sign = jest.fn().mockReturnValue('mock-token');
await authController.login(req, res);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({
success: true,
message: 'Login successful',
data: expect.objectContaining({
token: 'mock-token'
})
})
);
});
it('should login user successfully with username', async () => {
req.body = {
username: 'testuser',
password: 'Test123!@#'
};
const mockUser = {
id: '123',
username: 'testuser',
email: 'test@example.com',
password: 'hashed-password',
role: 'user',
isActive: true,
save: jest.fn()
};
User.findOne = jest.fn().mockResolvedValue(mockUser);
bcrypt.compare = jest.fn().mockResolvedValue(true);
jwt.sign = jest.fn().mockReturnValue('mock-token');
await authController.login(req, res);
expect(res.status).toHaveBeenCalledWith(200);
});
it('should return 400 if user not found', async () => {
req.body = {
email: 'nonexistent@example.com',
password: 'Test123!@#'
};
User.findOne = jest.fn().mockResolvedValue(null);
await authController.login(req, res);
expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({
success: false,
message: 'Invalid credentials'
})
);
});
it('should return 400 if password is incorrect', async () => {
req.body = {
email: 'test@example.com',
password: 'WrongPassword'
};
User.findOne = jest.fn().mockResolvedValue({
password: 'hashed-password'
});
bcrypt.compare = jest.fn().mockResolvedValue(false);
await authController.login(req, res);
expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({
success: false,
message: 'Invalid credentials'
})
);
});
it('should return 403 if user account is deactivated', async () => {
req.body = {
email: 'test@example.com',
password: 'Test123!@#'
};
User.findOne = jest.fn().mockResolvedValue({
isActive: false,
password: 'hashed-password'
});
bcrypt.compare = jest.fn().mockResolvedValue(true);
await authController.login(req, res);
expect(res.status).toHaveBeenCalledWith(403);
expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({
success: false,
message: 'Account is deactivated'
})
);
});
it('should return 400 for missing credentials', async () => {
req.body = {};
await authController.login(req, res);
expect(res.status).toHaveBeenCalledWith(400);
});
});
describe('logout', () => {
it('should logout user successfully', async () => {
req.user = { id: '123' };
await authController.logout(req, res);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({
success: true,
message: 'Logged out successfully'
})
);
});
it('should handle logout without user context', async () => {
req.user = null;
await authController.logout(req, res);
expect(res.status).toHaveBeenCalledWith(200);
});
});
describe('verifyToken', () => {
it('should verify user successfully', async () => {
req.user = {
id: '123',
username: 'testuser',
email: 'test@example.com',
role: 'user'
};
await authController.verifyToken(req, res);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({
success: true,
message: 'Token is valid',
data: expect.objectContaining({
user: expect.objectContaining({
id: '123',
username: 'testuser'
})
})
})
);
});
it('should return 401 if no user in request', async () => {
req.user = null;
await authController.verifyToken(req, res);
expect(res.status).toHaveBeenCalledWith(401);
expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({
success: false,
message: 'Unauthorized'
})
);
});
});
});

View File

@@ -0,0 +1,442 @@
const request = require('supertest');
const app = require('../server');
const { sequelize } = require('../models');
describe('Integration Tests - Complete User Flow', () => {
let server;
const testUser = {
username: 'integrationtest',
email: 'integration@test.com',
password: 'Test123!@#'
};
let userToken = null;
let adminToken = null;
beforeAll(async () => {
// Start server
server = app.listen(0);
// Login as admin for setup
const adminRes = await request(app)
.post('/api/auth/login')
.send({
email: 'admin@example.com',
password: 'Admin123!@#'
});
if (adminRes.status === 200) {
adminToken = adminRes.body.data.token;
}
});
afterAll(async () => {
// Close server and database connections
if (server) {
await new Promise(resolve => server.close(resolve));
}
await sequelize.close();
});
describe('1. User Registration Flow', () => {
it('should register a new user successfully', async () => {
const res = await request(app)
.post('/api/auth/register')
.send(testUser)
.expect(201);
expect(res.body.success).toBe(true);
expect(res.body.data.user.username).toBe(testUser.username);
expect(res.body.data.user.email).toBe(testUser.email);
expect(res.body.data.token).toBeDefined();
userToken = res.body.data.token;
});
it('should not register user with duplicate email', async () => {
const res = await request(app)
.post('/api/auth/register')
.send(testUser)
.expect(409);
expect(res.body.success).toBe(false);
expect(res.body.message).toContain('already');
});
it('should not register user with invalid email', async () => {
const res = await request(app)
.post('/api/auth/register')
.send({
username: 'testuser2',
email: 'invalid-email',
password: 'Test123!@#'
})
.expect(400);
expect(res.body.success).toBe(false);
});
it('should not register user with weak password', async () => {
const res = await request(app)
.post('/api/auth/register')
.send({
username: 'testuser3',
email: 'test3@example.com',
password: '123'
})
.expect(400);
expect(res.body.success).toBe(false);
});
});
describe('2. User Login Flow', () => {
it('should login with correct credentials', async () => {
const res = await request(app)
.post('/api/auth/login')
.send({
email: testUser.email,
password: testUser.password
})
.expect(200);
expect(res.body.success).toBe(true);
expect(res.body.data.token).toBeDefined();
expect(res.body.data.user.email).toBe(testUser.email);
});
it('should not login with incorrect password', async () => {
const res = await request(app)
.post('/api/auth/login')
.send({
email: testUser.email,
password: 'WrongPassword123'
})
.expect(401);
expect(res.body.success).toBe(false);
});
it('should not login with non-existent email', async () => {
const res = await request(app)
.post('/api/auth/login')
.send({
email: 'nonexistent@example.com',
password: 'Test123!@#'
})
.expect(401);
expect(res.body.success).toBe(false);
});
});
describe('3. Token Verification Flow', () => {
it('should verify valid token', async () => {
const res = await request(app)
.get('/api/auth/verify')
.set('Authorization', `Bearer ${userToken}`)
.expect(200);
expect(res.body.success).toBe(true);
expect(res.body.data.user).toBeDefined();
});
it('should reject invalid token', async () => {
const res = await request(app)
.get('/api/auth/verify')
.set('Authorization', 'Bearer invalid-token')
.expect(401);
expect(res.body.success).toBe(false);
});
it('should reject request without token', async () => {
const res = await request(app)
.get('/api/auth/verify')
.expect(401);
expect(res.body.success).toBe(false);
});
});
describe('4. Complete Quiz Flow', () => {
let quizSessionId = null;
let categoryId = null;
it('should get available categories', async () => {
const res = await request(app)
.get('/api/categories')
.set('Authorization', `Bearer ${userToken}`)
.expect(200);
expect(res.body.success).toBe(true);
expect(Array.isArray(res.body.data)).toBe(true);
if (res.body.data.length > 0) {
categoryId = res.body.data[0].id;
}
});
it('should start a quiz session', async () => {
if (!categoryId) {
console.log('Skipping quiz tests - no categories available');
return;
}
const res = await request(app)
.post('/api/quiz/start')
.set('Authorization', `Bearer ${userToken}`)
.send({
categoryId: categoryId,
quizType: 'practice',
difficulty: 'easy',
questionCount: 5
})
.expect(201);
expect(res.body.success).toBe(true);
expect(res.body.data.sessionId).toBeDefined();
quizSessionId = res.body.data.sessionId;
});
it('should get current quiz session', async () => {
if (!quizSessionId) {
return;
}
const res = await request(app)
.get(`/api/quiz/session/${quizSessionId}`)
.set('Authorization', `Bearer ${userToken}`)
.expect(200);
expect(res.body.success).toBe(true);
expect(res.body.data.id).toBe(quizSessionId);
});
it('should submit an answer', async () => {
if (!quizSessionId) {
return;
}
// Get the first question
const sessionRes = await request(app)
.get(`/api/quiz/session/${quizSessionId}`)
.set('Authorization', `Bearer ${userToken}`);
const questions = sessionRes.body.data.questions;
if (questions && questions.length > 0) {
const questionId = questions[0].id;
const correctOption = questions[0].correctOption;
const res = await request(app)
.post(`/api/quiz/session/${quizSessionId}/answer`)
.set('Authorization', `Bearer ${userToken}`)
.send({
questionId: questionId,
selectedOption: correctOption
})
.expect(200);
expect(res.body.success).toBe(true);
}
});
it('should complete quiz session', async () => {
if (!quizSessionId) {
return;
}
const res = await request(app)
.post(`/api/quiz/session/${quizSessionId}/complete`)
.set('Authorization', `Bearer ${userToken}`)
.expect(200);
expect(res.body.success).toBe(true);
expect(res.body.data.status).toBe('completed');
});
});
describe('5. Authorization Scenarios', () => {
it('should allow authenticated user to access protected route', async () => {
const res = await request(app)
.get('/api/user/profile')
.set('Authorization', `Bearer ${userToken}`)
.expect(200);
expect(res.body.success).toBe(true);
});
it('should deny unauthenticated access to protected route', async () => {
const res = await request(app)
.get('/api/user/profile')
.expect(401);
expect(res.body.success).toBe(false);
});
it('should deny non-admin access to admin route', async () => {
const res = await request(app)
.get('/api/admin/statistics')
.set('Authorization', `Bearer ${userToken}`)
.expect(403);
expect(res.body.success).toBe(false);
});
it('should allow admin access to admin route', async () => {
if (!adminToken) {
console.log('Skipping admin test - admin not logged in');
return;
}
const res = await request(app)
.get('/api/admin/statistics')
.set('Authorization', `Bearer ${adminToken}`)
.expect(200);
expect(res.body.success).toBe(true);
});
});
describe('6. Guest User Flow', () => {
let guestToken = null;
let guestId = null;
it('should create guest session', async () => {
const res = await request(app)
.post('/api/guest/session')
.expect(201);
expect(res.body.success).toBe(true);
expect(res.body.data.token).toBeDefined();
expect(res.body.data.guestId).toBeDefined();
guestToken = res.body.data.token;
guestId = res.body.data.guestId;
});
it('should allow guest to access public categories', async () => {
const res = await request(app)
.get('/api/guest/categories')
.set('Authorization', `Bearer ${guestToken}`)
.expect(200);
expect(res.body.success).toBe(true);
});
it('should convert guest to registered user', async () => {
if (!guestToken) {
return;
}
const res = await request(app)
.post('/api/guest/convert')
.set('Authorization', `Bearer ${guestToken}`)
.send({
username: 'convertedguest',
email: 'converted@guest.com',
password: 'Test123!@#'
})
.expect(201);
expect(res.body.success).toBe(true);
expect(res.body.data.user).toBeDefined();
expect(res.body.data.token).toBeDefined();
});
});
describe('7. Error Handling Scenarios', () => {
it('should return 404 for non-existent route', async () => {
const res = await request(app)
.get('/api/nonexistent')
.expect(404);
expect(res.body.success).toBe(false);
});
it('should handle malformed JSON', async () => {
const res = await request(app)
.post('/api/auth/login')
.set('Content-Type', 'application/json')
.send('invalid json{')
.expect(400);
});
it('should validate required fields', async () => {
const res = await request(app)
.post('/api/auth/register')
.send({
username: 'test'
// missing email and password
})
.expect(400);
expect(res.body.success).toBe(false);
});
it('should handle database errors gracefully', async () => {
// Try to access non-existent resource
const res = await request(app)
.get('/api/quiz/session/00000000-0000-0000-0000-000000000000')
.set('Authorization', `Bearer ${userToken}`)
.expect(404);
expect(res.body.success).toBe(false);
});
});
describe('8. User Profile Flow', () => {
it('should get user profile', async () => {
const res = await request(app)
.get('/api/user/profile')
.set('Authorization', `Bearer ${userToken}`)
.expect(200);
expect(res.body.success).toBe(true);
expect(res.body.data.username).toBe(testUser.username);
});
it('should get user statistics', async () => {
const res = await request(app)
.get('/api/user/statistics')
.set('Authorization', `Bearer ${userToken}`)
.expect(200);
expect(res.body.success).toBe(true);
expect(res.body.data.totalQuizzesTaken).toBeDefined();
});
it('should get quiz history', async () => {
const res = await request(app)
.get('/api/user/quiz-history')
.set('Authorization', `Bearer ${userToken}`)
.expect(200);
expect(res.body.success).toBe(true);
expect(Array.isArray(res.body.data.sessions)).toBe(true);
});
});
describe('9. Logout Flow', () => {
it('should logout successfully', async () => {
const res = await request(app)
.post('/api/auth/logout')
.set('Authorization', `Bearer ${userToken}`)
.expect(200);
expect(res.body.success).toBe(true);
});
it('should not access protected route after logout', async () => {
// Note: JWT tokens are stateless, so this test depends on token expiration
// In a real scenario with token blacklisting, this would fail
// For now, we just verify the logout endpoint works
const res = await request(app)
.post('/api/auth/logout')
.set('Authorization', `Bearer ${userToken}`)
.expect(200);
expect(res.body.success).toBe(true);
});
});
});