Fix jwt-token
This commit is contained in:
187
server/tests/middleware/auth.test.js
Normal file
187
server/tests/middleware/auth.test.js
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
286
server/tests/middleware/ip-restriction.test.js
Normal file
286
server/tests/middleware/ip-restriction.test.js
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
191
server/tests/middleware/multi-tenant-auth.test.js
Normal file
191
server/tests/middleware/multi-tenant-auth.test.js
Normal 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;
|
||||
});
|
||||
});
|
||||
});
|
||||
212
server/tests/middleware/rbac.test.js
Normal file
212
server/tests/middleware/rbac.test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
291
server/tests/middleware/validation.test.js
Normal file
291
server/tests/middleware/validation.test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user