Fix jwt-token

This commit is contained in:
2025-09-14 21:07:43 +02:00
parent d6293dd8ba
commit 019eb8c2b2
20 changed files with 7185 additions and 29 deletions

View File

@@ -0,0 +1,187 @@
const { describe, it, beforeEach, afterEach, before, after } = require('mocha');
const { expect } = require('chai');
const sinon = require('sinon');
const jwt = require('jsonwebtoken');
const { authenticateToken } = require('../../middleware/auth');
const { setupTestEnvironment, teardownTestEnvironment, cleanDatabase, mockRequest, mockResponse, mockNext, createTestUser, createTestTenant } = require('../setup');
describe('Authentication Middleware', () => {
let models, sequelize;
before(async () => {
({ models, sequelize } = await setupTestEnvironment());
});
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,
message: 'Access token required'
});
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,
message: 'Invalid token format'
});
});
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 token');
});
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('Token expired');
});
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 not found');
});
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('User account is inactive');
});
});
});

View File

@@ -0,0 +1,286 @@
const { describe, it, beforeEach, afterEach, before, after } = require('mocha');
const { expect } = require('chai');
const sinon = require('sinon');
const { checkIPRestriction } = require('../../middleware/ip-restriction');
const { setupTestEnvironment, teardownTestEnvironment, cleanDatabase, mockRequest, mockResponse, mockNext, createTestUser, createTestTenant } = require('../setup');
describe('IP Restriction Middleware', () => {
let models, sequelize;
before(async () => {
({ models, sequelize } = await setupTestEnvironment());
});
after(async () => {
await teardownTestEnvironment();
});
beforeEach(async () => {
await cleanDatabase();
});
describe('checkIPRestriction', () => {
it('should allow access when IP restrictions disabled', async () => {
const tenant = await createTestTenant({
slug: 'test-tenant',
ip_restrictions_enabled: false,
allowed_ips: '192.168.1.1,10.0.0.1'
});
const req = mockRequest({
ip: '127.0.0.1',
tenant: tenant.slug
});
const res = mockResponse();
const next = mockNext();
await checkIPRestriction(req, res, next);
expect(next.errors).to.have.length(0);
});
it('should allow access from allowed IP', async () => {
const tenant = await createTestTenant({
slug: 'test-tenant',
ip_restrictions_enabled: true,
allowed_ips: '192.168.1.1,10.0.0.1,127.0.0.1'
});
const req = mockRequest({
ip: '192.168.1.1',
tenant: tenant.slug
});
const res = mockResponse();
const next = mockNext();
await checkIPRestriction(req, res, next);
expect(next.errors).to.have.length(0);
});
it('should block access from non-allowed IP', async () => {
const tenant = await createTestTenant({
slug: 'test-tenant',
ip_restrictions_enabled: true,
allowed_ips: '192.168.1.1,10.0.0.1'
});
const req = mockRequest({
ip: '192.168.2.1',
tenant: tenant.slug
});
const res = mockResponse();
const next = mockNext();
await checkIPRestriction(req, res, next);
expect(res.statusCode).to.equal(403);
expect(res.data).to.deep.equal({
success: false,
message: 'Access denied: IP address not allowed',
ip: '192.168.2.1'
});
});
it('should allow access when tenant not found', async () => {
const req = mockRequest({
ip: '192.168.1.1',
tenant: 'nonexistent-tenant'
});
const res = mockResponse();
const next = mockNext();
await checkIPRestriction(req, res, next);
expect(next.errors).to.have.length(0);
});
it('should extract IP from x-forwarded-for header', async () => {
const tenant = await createTestTenant({
slug: 'test-tenant',
ip_restrictions_enabled: true,
allowed_ips: '203.0.113.1'
});
const req = mockRequest({
ip: '127.0.0.1', // Local proxy IP
headers: { 'x-forwarded-for': '203.0.113.1, 198.51.100.1' },
tenant: tenant.slug
});
const res = mockResponse();
const next = mockNext();
await checkIPRestriction(req, res, next);
expect(next.errors).to.have.length(0);
});
it('should extract IP from x-real-ip header', async () => {
const tenant = await createTestTenant({
slug: 'test-tenant',
ip_restrictions_enabled: true,
allowed_ips: '203.0.113.2'
});
const req = mockRequest({
ip: '127.0.0.1',
headers: { 'x-real-ip': '203.0.113.2' },
tenant: tenant.slug
});
const res = mockResponse();
const next = mockNext();
await checkIPRestriction(req, res, next);
expect(next.errors).to.have.length(0);
});
it('should handle CIDR notation in allowed IPs', async () => {
const tenant = await createTestTenant({
slug: 'test-tenant',
ip_restrictions_enabled: true,
allowed_ips: '192.168.1.0/24,10.0.0.0/8'
});
const req = mockRequest({
ip: '192.168.1.100',
tenant: tenant.slug
});
const res = mockResponse();
const next = mockNext();
await checkIPRestriction(req, res, next);
expect(next.errors).to.have.length(0);
});
it('should block IP outside CIDR range', async () => {
const tenant = await createTestTenant({
slug: 'test-tenant',
ip_restrictions_enabled: true,
allowed_ips: '192.168.1.0/24'
});
const req = mockRequest({
ip: '192.168.2.1',
tenant: tenant.slug
});
const res = mockResponse();
const next = mockNext();
await checkIPRestriction(req, res, next);
expect(res.statusCode).to.equal(403);
});
it('should allow access from Docker container networks', async () => {
const tenant = await createTestTenant({
slug: 'test-tenant',
ip_restrictions_enabled: true,
allowed_ips: '192.168.1.1'
});
const dockerIPs = ['172.17.0.1', '172.18.0.1', '172.19.0.1'];
for (const ip of dockerIPs) {
const req = mockRequest({
ip: ip,
tenant: tenant.slug
});
const res = mockResponse();
const next = mockNext();
await checkIPRestriction(req, res, next);
expect(next.errors).to.have.length(0);
}
});
it('should allow management routes regardless of IP restrictions', async () => {
const tenant = await createTestTenant({
slug: 'test-tenant',
ip_restrictions_enabled: true,
allowed_ips: '192.168.1.1'
});
const managementPaths = [
'/api/management/status',
'/api/management/health',
'/api/management/system-info'
];
for (const path of managementPaths) {
const req = mockRequest({
ip: '192.168.2.1', // Not in allowed IPs
path: path,
tenant: tenant.slug
});
const res = mockResponse();
const next = mockNext();
await checkIPRestriction(req, res, next);
expect(next.errors).to.have.length(0);
}
});
it('should handle empty allowed_ips list', async () => {
const tenant = await createTestTenant({
slug: 'test-tenant',
ip_restrictions_enabled: true,
allowed_ips: ''
});
const req = mockRequest({
ip: '192.168.1.1',
tenant: tenant.slug
});
const res = mockResponse();
const next = mockNext();
await checkIPRestriction(req, res, next);
expect(res.statusCode).to.equal(403);
});
it('should handle null allowed_ips', async () => {
const tenant = await createTestTenant({
slug: 'test-tenant',
ip_restrictions_enabled: true,
allowed_ips: null
});
const req = mockRequest({
ip: '192.168.1.1',
tenant: tenant.slug
});
const res = mockResponse();
const next = mockNext();
await checkIPRestriction(req, res, next);
expect(res.statusCode).to.equal(403);
});
it('should log IP restriction events', async () => {
const consoleLogSpy = sinon.spy(console, 'log');
const tenant = await createTestTenant({
slug: 'test-tenant',
ip_restrictions_enabled: true,
allowed_ips: '192.168.1.1'
});
const req = mockRequest({
ip: '192.168.2.1',
tenant: tenant.slug
});
const res = mockResponse();
const next = mockNext();
await checkIPRestriction(req, res, next);
expect(consoleLogSpy.calledWith(sinon.match(/🚫.*IP restriction/))).to.be.true;
consoleLogSpy.restore();
});
});
});

View File

@@ -0,0 +1,191 @@
const { describe, it, beforeEach, afterEach, before, after } = require('mocha');
const { expect } = require('chai');
const sinon = require('sinon');
const MultiTenantAuth = require('../../middleware/multi-tenant-auth');
const { setupTestEnvironment, teardownTestEnvironment, cleanDatabase, mockRequest, mockResponse, mockNext, createTestUser, createTestTenant, generateTestToken } = require('../setup');
describe('Multi-Tenant Authentication Middleware', () => {
let models, sequelize, multiAuth;
before(async () => {
({ models, sequelize } = await setupTestEnvironment());
multiAuth = new MultiTenantAuth();
});
after(async () => {
await teardownTestEnvironment();
});
beforeEach(async () => {
await cleanDatabase();
});
describe('determineTenant', () => {
it('should extract tenant from tenantId in JWT token', async () => {
const tenant = await createTestTenant({ slug: 'test-tenant' });
const user = await createTestUser({ tenant_id: tenant.id });
const req = mockRequest({
user: { tenantId: tenant.slug }
});
const result = await multiAuth.determineTenant(req);
expect(result).to.equal(tenant.slug);
});
it('should extract tenant from subdomain', async () => {
const req = mockRequest({
headers: { host: 'tenant1.example.com' }
});
const result = await multiAuth.determineTenant(req);
expect(result).to.equal('tenant1');
});
it('should extract tenant from complex subdomain', async () => {
const req = mockRequest({
headers: { host: 'uamils-ab.dev.uggla.uamils.com' }
});
const result = await multiAuth.determineTenant(req);
expect(result).to.equal('uamils-ab');
});
it('should extract tenant from domain path', async () => {
const req = mockRequest({
headers: { host: 'example.com' },
url: '/tenant2/api/devices'
});
const result = await multiAuth.determineTenant(req);
expect(result).to.equal('tenant2');
});
it('should prioritize JWT tenantId over subdomain', async () => {
const req = mockRequest({
user: { tenantId: 'jwt-tenant' },
headers: { host: 'subdomain-tenant.example.com' }
});
const result = await multiAuth.determineTenant(req);
expect(result).to.equal('jwt-tenant');
});
it('should return null for localhost without tenant info', async () => {
const req = mockRequest({
headers: { host: 'localhost:3000' }
});
const result = await multiAuth.determineTenant(req);
expect(result).to.be.null;
});
it('should handle x-forwarded-host header', async () => {
const req = mockRequest({
headers: {
host: 'localhost:3000',
'x-forwarded-host': 'tenant3.example.com'
}
});
const result = await multiAuth.determineTenant(req);
expect(result).to.equal('tenant3');
});
});
describe('middleware function', () => {
it('should pass through when tenant is determined', async () => {
const tenant = await createTestTenant({ slug: 'valid-tenant' });
const req = mockRequest({
user: { tenantId: tenant.slug }
});
const res = mockResponse();
const next = mockNext();
await multiAuth.middleware(req, res, next);
expect(req.tenant).to.equal(tenant.slug);
expect(next.errors).to.have.length(0);
});
it('should reject when tenant cannot be determined', async () => {
const req = mockRequest({
headers: { host: 'localhost:3000' }
});
const res = mockResponse();
const next = mockNext();
await multiAuth.middleware(req, res, next);
expect(res.statusCode).to.equal(400);
expect(res.data).to.deep.equal({
success: false,
message: 'Unable to determine tenant'
});
});
it('should reject when tenant does not exist in database', async () => {
const req = mockRequest({
user: { tenantId: 'nonexistent-tenant' }
});
const res = mockResponse();
const next = mockNext();
await multiAuth.middleware(req, res, next);
expect(res.statusCode).to.equal(404);
expect(res.data).to.deep.equal({
success: false,
message: 'Tenant not found'
});
});
it('should reject when tenant is inactive', async () => {
const tenant = await createTestTenant({
slug: 'inactive-tenant',
is_active: false
});
const req = mockRequest({
user: { tenantId: tenant.slug }
});
const res = mockResponse();
const next = mockNext();
await multiAuth.middleware(req, res, next);
expect(res.statusCode).to.equal(403);
expect(res.data).to.deep.equal({
success: false,
message: 'Tenant is not active'
});
});
});
describe('validateTenantAccess', () => {
it('should validate user belongs to tenant', async () => {
const tenant = await createTestTenant({ slug: 'user-tenant' });
const user = await createTestUser({ tenant_id: tenant.id });
const isValid = await multiAuth.validateTenantAccess(user.id, tenant.slug);
expect(isValid).to.be.true;
});
it('should reject user from different tenant', async () => {
const tenant1 = await createTestTenant({ slug: 'tenant1' });
const tenant2 = await createTestTenant({ slug: 'tenant2' });
const user = await createTestUser({ tenant_id: tenant1.id });
const isValid = await multiAuth.validateTenantAccess(user.id, tenant2.slug);
expect(isValid).to.be.false;
});
it('should reject nonexistent user', async () => {
const tenant = await createTestTenant({ slug: 'valid-tenant' });
const isValid = await multiAuth.validateTenantAccess(99999, tenant.slug);
expect(isValid).to.be.false;
});
});
});

View File

@@ -0,0 +1,212 @@
const { describe, it, beforeEach, afterEach, before, after } = require('mocha');
const { expect } = require('chai');
const sinon = require('sinon');
const { checkPermission, requirePermission } = require('../../middleware/rbac');
const { setupTestEnvironment, teardownTestEnvironment, cleanDatabase, mockRequest, mockResponse, mockNext, createTestUser, createTestTenant } = require('../setup');
describe('RBAC Middleware', () => {
let models, sequelize;
before(async () => {
({ models, sequelize } = await setupTestEnvironment());
});
after(async () => {
await teardownTestEnvironment();
});
beforeEach(async () => {
await cleanDatabase();
});
describe('checkPermission', () => {
it('should allow admin to access any resource', () => {
const result = checkPermission('admin', 'devices', 'read');
expect(result).to.be.true;
});
it('should allow user_admin to manage users', () => {
const result = checkPermission('user_admin', 'users', 'create');
expect(result).to.be.true;
});
it('should deny user_admin from managing devices', () => {
const result = checkPermission('user_admin', 'devices', 'create');
expect(result).to.be.false;
});
it('should allow security_admin to manage security features', () => {
expect(checkPermission('security_admin', 'alerts', 'create')).to.be.true;
expect(checkPermission('security_admin', 'ip_restrictions', 'read')).to.be.true;
expect(checkPermission('security_admin', 'audit_logs', 'read')).to.be.true;
});
it('should deny security_admin from managing users', () => {
const result = checkPermission('security_admin', 'users', 'create');
expect(result).to.be.false;
});
it('should allow branding_admin to manage branding', () => {
expect(checkPermission('branding_admin', 'branding', 'update')).to.be.true;
expect(checkPermission('branding_admin', 'ui_customization', 'create')).to.be.true;
});
it('should deny branding_admin from managing devices', () => {
const result = checkPermission('branding_admin', 'devices', 'delete');
expect(result).to.be.false;
});
it('should allow operator to read and create detections', () => {
expect(checkPermission('operator', 'devices', 'read')).to.be.true;
expect(checkPermission('operator', 'detections', 'read')).to.be.true;
expect(checkPermission('operator', 'detections', 'create')).to.be.true;
});
it('should deny operator from deleting devices', () => {
const result = checkPermission('operator', 'devices', 'delete');
expect(result).to.be.false;
});
it('should allow viewer to read only', () => {
expect(checkPermission('viewer', 'devices', 'read')).to.be.true;
expect(checkPermission('viewer', 'detections', 'read')).to.be.true;
expect(checkPermission('viewer', 'dashboard', 'read')).to.be.true;
});
it('should deny viewer from creating or updating', () => {
expect(checkPermission('viewer', 'devices', 'create')).to.be.false;
expect(checkPermission('viewer', 'devices', 'update')).to.be.false;
expect(checkPermission('viewer', 'detections', 'create')).to.be.false;
});
it('should deny unknown role', () => {
const result = checkPermission('unknown_role', 'devices', 'read');
expect(result).to.be.false;
});
it('should handle case-insensitive roles', () => {
const result = checkPermission('ADMIN', 'devices', 'read');
expect(result).to.be.true;
});
it('should handle undefined role', () => {
const result = checkPermission(undefined, 'devices', 'read');
expect(result).to.be.false;
});
});
describe('requirePermission middleware', () => {
it('should allow request with valid permission', async () => {
const req = mockRequest({
user: { role: 'admin' }
});
const res = mockResponse();
const next = mockNext();
const middleware = requirePermission('devices', 'read');
middleware(req, res, next);
expect(next.errors).to.have.length(0);
});
it('should deny request without permission', async () => {
const req = mockRequest({
user: { role: 'viewer' }
});
const res = mockResponse();
const next = mockNext();
const middleware = requirePermission('devices', 'delete');
middleware(req, res, next);
expect(res.statusCode).to.equal(403);
expect(res.data).to.deep.equal({
success: false,
message: 'Insufficient permissions'
});
});
it('should deny request without user', async () => {
const req = mockRequest({});
const res = mockResponse();
const next = mockNext();
const middleware = requirePermission('devices', 'read');
middleware(req, res, next);
expect(res.statusCode).to.equal(401);
expect(res.data).to.deep.equal({
success: false,
message: 'User not authenticated'
});
});
it('should deny request without user role', async () => {
const req = mockRequest({
user: { username: 'test' }
});
const res = mockResponse();
const next = mockNext();
const middleware = requirePermission('devices', 'read');
middleware(req, res, next);
expect(res.statusCode).to.equal(403);
expect(res.data).to.deep.equal({
success: false,
message: 'Insufficient permissions'
});
});
});
describe('Role-specific permission tests', () => {
const testCases = [
// Admin tests
{ role: 'admin', resource: 'devices', action: 'create', expected: true },
{ role: 'admin', resource: 'users', action: 'delete', expected: true },
{ role: 'admin', resource: 'tenants', action: 'update', expected: true },
// User Admin tests
{ role: 'user_admin', resource: 'users', action: 'create', expected: true },
{ role: 'user_admin', resource: 'users', action: 'update', expected: true },
{ role: 'user_admin', resource: 'users', action: 'delete', expected: true },
{ role: 'user_admin', resource: 'roles', action: 'read', expected: true },
{ role: 'user_admin', resource: 'devices', action: 'create', expected: false },
// Security Admin tests
{ role: 'security_admin', resource: 'alerts', action: 'create', expected: true },
{ role: 'security_admin', resource: 'ip_restrictions', action: 'update', expected: true },
{ role: 'security_admin', resource: 'audit_logs', action: 'read', expected: true },
{ role: 'security_admin', resource: 'users', action: 'create', expected: false },
// Branding Admin tests
{ role: 'branding_admin', resource: 'branding', action: 'update', expected: true },
{ role: 'branding_admin', resource: 'ui_customization', action: 'create', expected: true },
{ role: 'branding_admin', resource: 'logo', action: 'upload', expected: true },
{ role: 'branding_admin', resource: 'devices', action: 'create', expected: false },
// Operator tests
{ role: 'operator', resource: 'devices', action: 'read', expected: true },
{ role: 'operator', resource: 'devices', action: 'update', expected: true },
{ role: 'operator', resource: 'detections', action: 'read', expected: true },
{ role: 'operator', resource: 'detections', action: 'create', expected: true },
{ role: 'operator', resource: 'devices', action: 'delete', expected: false },
{ role: 'operator', resource: 'users', action: 'create', expected: false },
// Viewer tests
{ role: 'viewer', resource: 'devices', action: 'read', expected: true },
{ role: 'viewer', resource: 'detections', action: 'read', expected: true },
{ role: 'viewer', resource: 'dashboard', action: 'read', expected: true },
{ role: 'viewer', resource: 'devices', action: 'create', expected: false },
{ role: 'viewer', resource: 'devices', action: 'update', expected: false },
{ role: 'viewer', resource: 'users', action: 'read', expected: false }
];
testCases.forEach(({ role, resource, action, expected }) => {
it(`should ${expected ? 'allow' : 'deny'} ${role} to ${action} ${resource}`, () => {
const result = checkPermission(role, resource, action);
expect(result).to.equal(expected);
});
});
});
});

View File

@@ -0,0 +1,291 @@
const { describe, it, beforeEach, afterEach, before, after } = require('mocha');
const { expect } = require('chai');
const sinon = require('sinon');
const { validateRequest } = require('../../middleware/validation');
const Joi = require('joi');
const { mockRequest, mockResponse, mockNext } = require('../setup');
describe('Validation Middleware', () => {
describe('validateRequest', () => {
const testSchema = Joi.object({
name: Joi.string().required(),
email: Joi.string().email().required(),
age: Joi.number().integer().min(0).max(120).optional(),
tags: Joi.array().items(Joi.string()).optional()
});
it('should pass validation with valid data', () => {
const req = mockRequest({
body: {
name: 'John Doe',
email: 'john@example.com',
age: 30,
tags: ['admin', 'user']
}
});
const res = mockResponse();
const next = mockNext();
const middleware = validateRequest(testSchema);
middleware(req, res, next);
expect(next.errors).to.have.length(0);
expect(req.body).to.deep.equal({
name: 'John Doe',
email: 'john@example.com',
age: 30,
tags: ['admin', 'user']
});
});
it('should fail validation with missing required field', () => {
const req = mockRequest({
body: {
email: 'john@example.com'
// missing name
}
});
const res = mockResponse();
const next = mockNext();
const middleware = validateRequest(testSchema);
middleware(req, res, next);
expect(res.statusCode).to.equal(400);
expect(res.data.success).to.be.false;
expect(res.data.message).to.include('name');
});
it('should fail validation with invalid email', () => {
const req = mockRequest({
body: {
name: 'John Doe',
email: 'invalid-email'
}
});
const res = mockResponse();
const next = mockNext();
const middleware = validateRequest(testSchema);
middleware(req, res, next);
expect(res.statusCode).to.equal(400);
expect(res.data.success).to.be.false;
expect(res.data.message).to.include('email');
});
it('should fail validation with invalid age', () => {
const req = mockRequest({
body: {
name: 'John Doe',
email: 'john@example.com',
age: -5
}
});
const res = mockResponse();
const next = mockNext();
const middleware = validateRequest(testSchema);
middleware(req, res, next);
expect(res.statusCode).to.equal(400);
expect(res.data.success).to.be.false;
expect(res.data.message).to.include('age');
});
it('should strip unknown fields', () => {
const req = mockRequest({
body: {
name: 'John Doe',
email: 'john@example.com',
unknownField: 'should be removed'
}
});
const res = mockResponse();
const next = mockNext();
const middleware = validateRequest(testSchema);
middleware(req, res, next);
expect(next.errors).to.have.length(0);
expect(req.body).to.not.have.property('unknownField');
});
it('should handle array validation', () => {
const req = mockRequest({
body: {
name: 'John Doe',
email: 'john@example.com',
tags: ['valid', 'tags']
}
});
const res = mockResponse();
const next = mockNext();
const middleware = validateRequest(testSchema);
middleware(req, res, next);
expect(next.errors).to.have.length(0);
expect(req.body.tags).to.deep.equal(['valid', 'tags']);
});
it('should fail with invalid array items', () => {
const req = mockRequest({
body: {
name: 'John Doe',
email: 'john@example.com',
tags: ['valid', 123, 'invalid-number']
}
});
const res = mockResponse();
const next = mockNext();
const middleware = validateRequest(testSchema);
middleware(req, res, next);
expect(res.statusCode).to.equal(400);
expect(res.data.success).to.be.false;
});
it('should handle nested object validation', () => {
const nestedSchema = Joi.object({
user: Joi.object({
name: Joi.string().required(),
profile: Joi.object({
age: Joi.number().required(),
preferences: Joi.array().items(Joi.string())
}).required()
}).required()
});
const req = mockRequest({
body: {
user: {
name: 'John',
profile: {
age: 30,
preferences: ['dark-mode', 'notifications']
}
}
}
});
const res = mockResponse();
const next = mockNext();
const middleware = validateRequest(nestedSchema);
middleware(req, res, next);
expect(next.errors).to.have.length(0);
});
it('should provide detailed error messages', () => {
const req = mockRequest({
body: {
name: '',
email: 'invalid',
age: 150
}
});
const res = mockResponse();
const next = mockNext();
const middleware = validateRequest(testSchema);
middleware(req, res, next);
expect(res.statusCode).to.equal(400);
expect(res.data.success).to.be.false;
expect(res.data.message).to.be.a('string');
expect(res.data.details).to.be.an('array');
expect(res.data.details.length).to.be.greaterThan(0);
});
it('should handle alternative schemas', () => {
const altSchema = Joi.alternatives().try(
Joi.object({
type: Joi.string().valid('user').required(),
username: Joi.string().required()
}),
Joi.object({
type: Joi.string().valid('device').required(),
deviceId: Joi.number().required()
})
);
const req1 = mockRequest({
body: {
type: 'user',
username: 'john'
}
});
const res1 = mockResponse();
const next1 = mockNext();
const middleware1 = validateRequest(altSchema);
middleware1(req1, res1, next1);
expect(next1.errors).to.have.length(0);
const req2 = mockRequest({
body: {
type: 'device',
deviceId: 123
}
});
const res2 = mockResponse();
const next2 = mockNext();
const middleware2 = validateRequest(altSchema);
middleware2(req2, res2, next2);
expect(next2.errors).to.have.length(0);
});
it('should handle query parameter validation', () => {
const querySchema = Joi.object({
page: Joi.number().integer().min(1).default(1),
limit: Joi.number().integer().min(1).max(100).default(10),
search: Joi.string().optional()
});
const req = mockRequest({
query: {
page: '2',
limit: '20',
search: 'test'
}
});
const res = mockResponse();
const next = mockNext();
const middleware = validateRequest(querySchema, 'query');
middleware(req, res, next);
expect(next.errors).to.have.length(0);
expect(req.query.page).to.equal(2); // Should be converted to number
expect(req.query.limit).to.equal(20);
});
it('should handle params validation', () => {
const paramsSchema = Joi.object({
id: Joi.number().integer().positive().required(),
slug: Joi.string().alphanum().optional()
});
const req = mockRequest({
params: {
id: '123',
slug: 'test-slug'
}
});
const res = mockResponse();
const next = mockNext();
const middleware = validateRequest(paramsSchema, 'params');
middleware(req, res, next);
expect(next.errors).to.have.length(0);
expect(req.params.id).to.equal(123);
});
});
});