373 lines
11 KiB
JavaScript
373 lines
11 KiB
JavaScript
// CRITICAL: Set environment variables FIRST
|
|
process.env.NODE_ENV = 'test';
|
|
process.env.JWT_SECRET = 'test-jwt-secret-key-for-testing-only';
|
|
|
|
const { describe, it, beforeEach, afterEach, before, after } = require('mocha');
|
|
const { expect } = require('chai');
|
|
const sinon = require('sinon');
|
|
const jwt = require('jsonwebtoken');
|
|
const { setupTestEnvironment, teardownTestEnvironment, cleanDatabase, mockRequest, mockResponse, mockNext, createTestUser, createTestTenant } = require('../setup');
|
|
const { authenticateToken, requireRole, setModels } = require('../../middleware/auth');
|
|
|
|
describe('Authentication Middleware', () => {
|
|
let models, sequelize;
|
|
|
|
before(async () => {
|
|
({ models, sequelize } = await setupTestEnvironment());
|
|
setModels(models); // Set models for the auth middleware
|
|
});
|
|
|
|
after(async () => {
|
|
await teardownTestEnvironment();
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
await cleanDatabase();
|
|
});
|
|
|
|
describe('authenticateToken', () => {
|
|
it('should reject request without Authorization header', async () => {
|
|
const req = mockRequest();
|
|
const res = mockResponse();
|
|
const next = mockNext();
|
|
|
|
await authenticateToken(req, res, next);
|
|
|
|
expect(res.statusCode).to.equal(401);
|
|
expect(res.data).to.deep.equal({
|
|
success: false,
|
|
error: 'NO_TOKEN',
|
|
message: 'No authentication token provided.',
|
|
redirectToLogin: true
|
|
});
|
|
expect(next.errors).to.have.length(0);
|
|
});
|
|
|
|
it('should reject request with invalid token format', async () => {
|
|
const req = mockRequest({
|
|
headers: { authorization: 'InvalidToken' }
|
|
});
|
|
const res = mockResponse();
|
|
const next = mockNext();
|
|
|
|
await authenticateToken(req, res, next);
|
|
|
|
expect(res.statusCode).to.equal(401);
|
|
expect(res.data).to.deep.equal({
|
|
success: false,
|
|
error: 'INVALID_TOKEN',
|
|
message: 'Invalid authentication token. Please log in again.',
|
|
redirectToLogin: true
|
|
});
|
|
});
|
|
|
|
it('should reject request with invalid JWT token', async () => {
|
|
const req = mockRequest({
|
|
headers: { authorization: 'Bearer invalid.jwt.token' }
|
|
});
|
|
const res = mockResponse();
|
|
const next = mockNext();
|
|
|
|
await authenticateToken(req, res, next);
|
|
|
|
expect(res.statusCode).to.equal(401);
|
|
expect(res.data.success).to.be.false;
|
|
expect(res.data.message).to.equal('Invalid authentication token. Please log in again.');
|
|
});
|
|
|
|
it('should reject request with expired JWT token', async () => {
|
|
const expiredToken = jwt.sign(
|
|
{ userId: 1, username: 'test' },
|
|
process.env.JWT_SECRET || 'test-secret',
|
|
{ expiresIn: '-1h' }
|
|
);
|
|
|
|
const req = mockRequest({
|
|
headers: { authorization: `Bearer ${expiredToken}` }
|
|
});
|
|
const res = mockResponse();
|
|
const next = mockNext();
|
|
|
|
await authenticateToken(req, res, next);
|
|
|
|
expect(res.statusCode).to.equal(401);
|
|
expect(res.data.success).to.be.false;
|
|
expect(res.data.message).to.equal('Your session has expired. Please log in again.');
|
|
});
|
|
|
|
it('should accept valid JWT token and set user data', async () => {
|
|
const user = await createTestUser({ username: 'testuser', role: 'admin' });
|
|
const token = jwt.sign(
|
|
{
|
|
userId: user.id,
|
|
username: user.username,
|
|
role: user.role,
|
|
email: user.email
|
|
},
|
|
process.env.JWT_SECRET || 'test-secret',
|
|
{ expiresIn: '1h' }
|
|
);
|
|
|
|
const req = mockRequest({
|
|
headers: { authorization: `Bearer ${token}` }
|
|
});
|
|
const res = mockResponse();
|
|
const next = mockNext();
|
|
|
|
await authenticateToken(req, res, next);
|
|
|
|
expect(req.user).to.exist;
|
|
expect(req.user.userId).to.equal(user.id);
|
|
expect(req.user.username).to.equal(user.username);
|
|
expect(req.user.role).to.equal(user.role);
|
|
expect(next.errors).to.have.length(0);
|
|
});
|
|
|
|
it('should handle token with tenantId', async () => {
|
|
const tenant = await createTestTenant({ slug: 'test-tenant' });
|
|
const user = await createTestUser({ username: 'testuser', tenant_id: tenant.id });
|
|
const token = jwt.sign(
|
|
{
|
|
userId: user.id,
|
|
username: user.username,
|
|
role: user.role,
|
|
tenantId: tenant.slug
|
|
},
|
|
process.env.JWT_SECRET || 'test-secret',
|
|
{ expiresIn: '1h' }
|
|
);
|
|
|
|
const req = mockRequest({
|
|
headers: { authorization: `Bearer ${token}` }
|
|
});
|
|
const res = mockResponse();
|
|
const next = mockNext();
|
|
|
|
await authenticateToken(req, res, next);
|
|
|
|
expect(req.user.tenantId).to.equal(tenant.slug);
|
|
expect(next.errors).to.have.length(0);
|
|
});
|
|
|
|
it('should reject user not found in database', async () => {
|
|
const token = jwt.sign(
|
|
{ userId: 99999, username: 'nonexistent' },
|
|
process.env.JWT_SECRET || 'test-secret',
|
|
{ expiresIn: '1h' }
|
|
);
|
|
|
|
const req = mockRequest({
|
|
headers: { authorization: `Bearer ${token}` }
|
|
});
|
|
const res = mockResponse();
|
|
const next = mockNext();
|
|
|
|
await authenticateToken(req, res, next);
|
|
|
|
expect(res.statusCode).to.equal(401);
|
|
expect(res.data.success).to.be.false;
|
|
expect(res.data.message).to.equal('User account not found. Please contact support.');
|
|
});
|
|
|
|
it('should reject inactive user', async () => {
|
|
const user = await createTestUser({
|
|
username: 'inactive',
|
|
is_active: false
|
|
});
|
|
const token = jwt.sign(
|
|
{ userId: user.id, username: user.username },
|
|
process.env.JWT_SECRET || 'test-secret',
|
|
{ expiresIn: '1h' }
|
|
);
|
|
|
|
const req = mockRequest({
|
|
headers: { authorization: `Bearer ${token}` }
|
|
});
|
|
const res = mockResponse();
|
|
const next = mockNext();
|
|
|
|
await authenticateToken(req, res, next);
|
|
|
|
expect(res.statusCode).to.equal(401);
|
|
expect(res.data.success).to.be.false;
|
|
expect(res.data.message).to.equal('Your account has been deactivated. Please contact support.');
|
|
});
|
|
|
|
it('should handle malformed JWT token', async () => {
|
|
const req = mockRequest({
|
|
headers: { authorization: 'Bearer malformed.jwt' }
|
|
});
|
|
const res = mockResponse();
|
|
const next = mockNext();
|
|
|
|
await authenticateToken(req, res, next);
|
|
|
|
expect(res.statusCode).to.equal(401);
|
|
expect(res.data.success).to.be.false;
|
|
expect(res.data.error).to.equal('INVALID_TOKEN');
|
|
expect(res.data.redirectToLogin).to.be.true;
|
|
});
|
|
|
|
it('should handle JWT with invalid signature', async () => {
|
|
const invalidToken = jwt.sign(
|
|
{ userId: 1, username: 'test' },
|
|
'wrong-secret',
|
|
{ expiresIn: '1h' }
|
|
);
|
|
|
|
const req = mockRequest({
|
|
headers: { authorization: `Bearer ${invalidToken}` }
|
|
});
|
|
const res = mockResponse();
|
|
const next = mockNext();
|
|
|
|
await authenticateToken(req, res, next);
|
|
|
|
expect(res.statusCode).to.equal(401);
|
|
expect(res.data.success).to.be.false;
|
|
expect(res.data.error).to.equal('INVALID_TOKEN');
|
|
expect(res.data.redirectToLogin).to.be.true;
|
|
});
|
|
|
|
it('should handle JWT not before error', async () => {
|
|
const futureToken = jwt.sign(
|
|
{ userId: 1, username: 'test', nbf: Math.floor(Date.now() / 1000) + 3600 },
|
|
process.env.JWT_SECRET || 'test-secret',
|
|
{ expiresIn: '1h' }
|
|
);
|
|
|
|
const req = mockRequest({
|
|
headers: { authorization: `Bearer ${futureToken}` }
|
|
});
|
|
const res = mockResponse();
|
|
const next = mockNext();
|
|
|
|
await authenticateToken(req, res, next);
|
|
|
|
expect(res.statusCode).to.equal(401);
|
|
expect(res.data.success).to.be.false;
|
|
expect(res.data.error).to.equal('TOKEN_NOT_ACTIVE');
|
|
expect(res.data.redirectToLogin).to.be.true;
|
|
});
|
|
|
|
it('should handle missing JWT_SECRET environment variable', async () => {
|
|
const originalSecret = process.env.JWT_SECRET;
|
|
delete process.env.JWT_SECRET;
|
|
|
|
const token = jwt.sign(
|
|
{ userId: 1, username: 'test' },
|
|
'any-secret',
|
|
{ expiresIn: '1h' }
|
|
);
|
|
|
|
const req = mockRequest({
|
|
headers: { authorization: `Bearer ${token}` }
|
|
});
|
|
const res = mockResponse();
|
|
const next = mockNext();
|
|
|
|
await authenticateToken(req, res, next);
|
|
|
|
expect(res.statusCode).to.equal(401);
|
|
expect(res.data.success).to.be.false;
|
|
expect(res.data.error).to.equal('AUTHENTICATION_FAILED');
|
|
expect(res.data.redirectToLogin).to.be.true;
|
|
|
|
// Restore original secret
|
|
process.env.JWT_SECRET = originalSecret;
|
|
});
|
|
|
|
it('should include error details for monitoring', async () => {
|
|
const originalEnv = process.env.NODE_ENV;
|
|
process.env.NODE_ENV = 'production';
|
|
|
|
const consoleSpy = sinon.spy(console, 'error');
|
|
|
|
const req = mockRequest({
|
|
headers: {
|
|
authorization: 'Bearer invalid.jwt.token',
|
|
'user-agent': 'Test Agent',
|
|
},
|
|
ip: '127.0.0.1',
|
|
path: '/api/test',
|
|
connection: { remoteAddress: '127.0.0.1' }
|
|
});
|
|
const res = mockResponse();
|
|
const next = mockNext();
|
|
|
|
await authenticateToken(req, res, next);
|
|
|
|
expect(consoleSpy.calledOnce).to.be.true;
|
|
const logCall = consoleSpy.firstCall.args;
|
|
expect(logCall[0]).to.equal('🔐 Authentication error:');
|
|
expect(logCall[1]).to.have.property('error');
|
|
expect(logCall[1]).to.have.property('userAgent', 'Test Agent');
|
|
expect(logCall[1]).to.have.property('ip', '127.0.0.1');
|
|
expect(logCall[1]).to.have.property('path', '/api/test');
|
|
|
|
consoleSpy.restore();
|
|
process.env.NODE_ENV = originalEnv;
|
|
});
|
|
});
|
|
|
|
describe('Enhanced Error Response Format', () => {
|
|
it('should return consistent error format for expired tokens', async () => {
|
|
const expiredToken = jwt.sign(
|
|
{ userId: 1, username: 'test' },
|
|
process.env.JWT_SECRET || 'test-secret',
|
|
{ expiresIn: '-1h' }
|
|
);
|
|
|
|
const req = mockRequest({
|
|
headers: { authorization: `Bearer ${expiredToken}` }
|
|
});
|
|
const res = mockResponse();
|
|
const next = mockNext();
|
|
|
|
await authenticateToken(req, res, next);
|
|
|
|
expect(res.statusCode).to.equal(401);
|
|
expect(res.data).to.deep.equal({
|
|
success: false,
|
|
error: 'TOKEN_EXPIRED',
|
|
message: 'Your session has expired. Please log in again.',
|
|
redirectToLogin: true
|
|
});
|
|
});
|
|
|
|
it('should return consistent error format for invalid tokens', async () => {
|
|
const req = mockRequest({
|
|
headers: { authorization: 'Bearer invalid.jwt.token' }
|
|
});
|
|
const res = mockResponse();
|
|
const next = mockNext();
|
|
|
|
await authenticateToken(req, res, next);
|
|
|
|
expect(res.statusCode).to.equal(401);
|
|
expect(res.data).to.deep.equal({
|
|
success: false,
|
|
error: 'INVALID_TOKEN',
|
|
message: 'Invalid authentication token. Please log in again.',
|
|
redirectToLogin: true
|
|
});
|
|
});
|
|
|
|
it('should return consistent error format for missing tokens', async () => {
|
|
const req = mockRequest();
|
|
const res = mockResponse();
|
|
const next = mockNext();
|
|
|
|
await authenticateToken(req, res, next);
|
|
|
|
expect(res.statusCode).to.equal(401);
|
|
expect(res.data).to.deep.equal({
|
|
success: false,
|
|
error: 'NO_TOKEN',
|
|
message: 'No authentication token provided.',
|
|
redirectToLogin: true
|
|
});
|
|
});
|
|
});
|
|
});
|