453 lines
16 KiB
JavaScript
453 lines
16 KiB
JavaScript
const { describe, it, beforeEach, afterEach, before, after } = require('mocha');
|
|
const { expect } = require('chai');
|
|
const sinon = require('sinon');
|
|
const request = require('supertest');
|
|
const express = require('express');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { setupTestEnvironment, teardownTestEnvironment, cleanDatabase, createTestUser, createTestTenant, generateTestToken } = require('../setup');
|
|
const tenantRoutes = require('../../routes/tenant');
|
|
const { SecurityLogger } = require('../../middleware/logger');
|
|
|
|
describe('Tenant Logo Management', () => {
|
|
let app, models, sequelize;
|
|
let testTenant, testUser, testUserToken;
|
|
let adminUser, adminUserToken;
|
|
let otherTenantUser, otherTenantUserToken;
|
|
let securityLoggerStub;
|
|
|
|
before(async () => {
|
|
({ models, sequelize } = await setupTestEnvironment());
|
|
|
|
// Setup express app for testing
|
|
app = express();
|
|
app.use(express.json());
|
|
app.use('/tenant', tenantRoutes);
|
|
|
|
// Create uploads directory for testing
|
|
const uploadsDir = path.join(__dirname, '../../uploads/logos');
|
|
if (!fs.existsSync(uploadsDir)) {
|
|
fs.mkdirSync(uploadsDir, { recursive: true });
|
|
}
|
|
});
|
|
|
|
after(async () => {
|
|
// Clean up test uploads directory
|
|
const uploadsDir = path.join(__dirname, '../../uploads/logos');
|
|
if (fs.existsSync(uploadsDir)) {
|
|
const files = fs.readdirSync(uploadsDir);
|
|
files.forEach(file => {
|
|
if (file.startsWith('tenant-')) {
|
|
fs.unlinkSync(path.join(uploadsDir, file));
|
|
}
|
|
});
|
|
}
|
|
|
|
await teardownTestEnvironment();
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
await cleanDatabase();
|
|
|
|
// Stub SecurityLogger to capture audit logs
|
|
securityLoggerStub = sinon.stub(SecurityLogger.prototype, 'logSecurityEvent');
|
|
|
|
// Create test tenant
|
|
testTenant = await createTestTenant({
|
|
slug: 'test-tenant',
|
|
name: 'Test Tenant',
|
|
branding: {}
|
|
});
|
|
|
|
// Create test users with different roles
|
|
testUser = await createTestUser({
|
|
username: 'testuser',
|
|
tenant_id: testTenant.id,
|
|
role: 'tenant_user'
|
|
});
|
|
testUserToken = generateTestToken(testUser);
|
|
|
|
adminUser = await createTestUser({
|
|
username: 'adminuser',
|
|
tenant_id: testTenant.id,
|
|
role: 'tenant_admin'
|
|
});
|
|
adminUserToken = generateTestToken(adminUser);
|
|
|
|
// Create user from different tenant
|
|
const otherTenant = await createTestTenant({
|
|
slug: 'other-tenant',
|
|
name: 'Other Tenant'
|
|
});
|
|
otherTenantUser = await createTestUser({
|
|
username: 'otheruser',
|
|
tenant_id: otherTenant.id,
|
|
role: 'tenant_admin'
|
|
});
|
|
otherTenantUserToken = generateTestToken(otherTenantUser);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
if (securityLoggerStub) {
|
|
securityLoggerStub.restore();
|
|
}
|
|
});
|
|
|
|
describe('POST /tenant/logo-upload', () => {
|
|
const testImagePath = path.join(__dirname, '../fixtures/test-logo.png');
|
|
|
|
beforeEach(() => {
|
|
// Create a test image file if it doesn't exist
|
|
if (!fs.existsSync(testImagePath)) {
|
|
const testImageBuffer = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==', 'base64');
|
|
fs.writeFileSync(testImagePath, testImageBuffer);
|
|
}
|
|
});
|
|
|
|
it('should successfully upload logo with valid permissions', async () => {
|
|
const response = await request(app)
|
|
.post('/tenant/logo-upload')
|
|
.set('Host', 'test-tenant.example.com')
|
|
.set('Authorization', `Bearer ${adminUserToken}`)
|
|
.attach('logo', testImagePath);
|
|
|
|
expect(response.status).to.equal(200);
|
|
expect(response.body.success).to.be.true;
|
|
expect(response.body.message).to.equal('Logo uploaded successfully');
|
|
expect(response.body.data.logo_url).to.include('/uploads/logos/');
|
|
|
|
// Verify audit log was created
|
|
expect(securityLoggerStub.calledWith('info', 'Tenant logo uploaded successfully')).to.be.true;
|
|
});
|
|
|
|
it('should reject upload without authentication', async () => {
|
|
const response = await request(app)
|
|
.post('/tenant/logo-upload')
|
|
.set('Host', 'test-tenant.example.com')
|
|
.attach('logo', testImagePath);
|
|
|
|
expect(response.status).to.equal(401);
|
|
expect(response.body.success).to.be.false;
|
|
});
|
|
|
|
it('should reject upload without required permissions', async () => {
|
|
const response = await request(app)
|
|
.post('/tenant/logo-upload')
|
|
.set('Host', 'test-tenant.example.com')
|
|
.set('Authorization', `Bearer ${testUserToken}`)
|
|
.attach('logo', testImagePath);
|
|
|
|
expect(response.status).to.equal(403);
|
|
expect(response.body.success).to.be.false;
|
|
});
|
|
|
|
it('should reject upload from user of different tenant', async () => {
|
|
const response = await request(app)
|
|
.post('/tenant/logo-upload')
|
|
.set('Host', 'test-tenant.example.com')
|
|
.set('Authorization', `Bearer ${otherTenantUserToken}`)
|
|
.attach('logo', testImagePath);
|
|
|
|
expect(response.status).to.equal(403);
|
|
expect(response.body.success).to.be.false;
|
|
expect(response.body.message).to.equal('Access denied: User not member of tenant');
|
|
|
|
// Verify security audit log
|
|
expect(securityLoggerStub.calledWith('warning', 'User attempted logo_upload on different tenant')).to.be.true;
|
|
});
|
|
|
|
it('should reject upload without file', async () => {
|
|
const response = await request(app)
|
|
.post('/tenant/logo-upload')
|
|
.set('Host', 'test-tenant.example.com')
|
|
.set('Authorization', `Bearer ${adminUserToken}`);
|
|
|
|
expect(response.status).to.equal(400);
|
|
expect(response.body.success).to.be.false;
|
|
expect(response.body.message).to.equal('No file uploaded');
|
|
|
|
// Verify security audit log
|
|
expect(securityLoggerStub.calledWith('warning', 'Logo upload attempted without file')).to.be.true;
|
|
});
|
|
|
|
it('should replace existing logo when uploading new one', async () => {
|
|
// First upload
|
|
const firstResponse = await request(app)
|
|
.post('/tenant/logo-upload')
|
|
.set('Host', 'test-tenant.example.com')
|
|
.set('Authorization', `Bearer ${adminUserToken}`)
|
|
.attach('logo', testImagePath);
|
|
|
|
expect(firstResponse.status).to.equal(200);
|
|
const firstLogoUrl = firstResponse.body.data.logo_url;
|
|
|
|
// Second upload
|
|
const secondResponse = await request(app)
|
|
.post('/tenant/logo-upload')
|
|
.set('Host', 'test-tenant.example.com')
|
|
.set('Authorization', `Bearer ${adminUserToken}`)
|
|
.attach('logo', testImagePath);
|
|
|
|
expect(secondResponse.status).to.equal(200);
|
|
const secondLogoUrl = secondResponse.body.data.logo_url;
|
|
|
|
expect(firstLogoUrl).to.not.equal(secondLogoUrl);
|
|
|
|
// Verify old logo cleanup was logged
|
|
expect(securityLoggerStub.calledWith('info', 'Old logo file cleaned up')).to.be.true;
|
|
});
|
|
|
|
it('should validate tenant access and log appropriately', async () => {
|
|
const response = await request(app)
|
|
.post('/tenant/logo-upload')
|
|
.set('Host', 'test-tenant.example.com')
|
|
.set('Authorization', `Bearer ${adminUserToken}`)
|
|
.attach('logo', testImagePath);
|
|
|
|
expect(response.status).to.equal(200);
|
|
|
|
// Verify access validation log
|
|
expect(securityLoggerStub.calledWith('info', 'logo_upload access validated')).to.be.true;
|
|
});
|
|
});
|
|
|
|
describe('DELETE /tenant/logo', () => {
|
|
let logoUrl;
|
|
|
|
beforeEach(async () => {
|
|
// Upload a logo first
|
|
const testImagePath = path.join(__dirname, '../fixtures/test-logo.png');
|
|
if (!fs.existsSync(testImagePath)) {
|
|
const testImageBuffer = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==', 'base64');
|
|
fs.writeFileSync(testImagePath, testImageBuffer);
|
|
}
|
|
|
|
const uploadResponse = await request(app)
|
|
.post('/tenant/logo-upload')
|
|
.set('Host', 'test-tenant.example.com')
|
|
.set('Authorization', `Bearer ${adminUserToken}`)
|
|
.attach('logo', testImagePath);
|
|
|
|
logoUrl = uploadResponse.body.data.logo_url;
|
|
|
|
// Reset stub to clear upload logs
|
|
securityLoggerStub.resetHistory();
|
|
});
|
|
|
|
it('should successfully remove logo with valid permissions', async () => {
|
|
const response = await request(app)
|
|
.delete('/tenant/logo')
|
|
.set('Host', 'test-tenant.example.com')
|
|
.set('Authorization', `Bearer ${adminUserToken}`);
|
|
|
|
expect(response.status).to.equal(200);
|
|
expect(response.body.success).to.be.true;
|
|
expect(response.body.message).to.equal('Logo removed successfully');
|
|
expect(response.body.data.branding.logo_url).to.be.null;
|
|
|
|
// Verify audit logs
|
|
expect(securityLoggerStub.calledWith('info', 'Tenant logo removed successfully')).to.be.true;
|
|
expect(securityLoggerStub.calledWith('info', 'Logo file deleted from filesystem')).to.be.true;
|
|
});
|
|
|
|
it('should reject removal without authentication', async () => {
|
|
const response = await request(app)
|
|
.delete('/tenant/logo')
|
|
.set('Host', 'test-tenant.example.com');
|
|
|
|
expect(response.status).to.equal(401);
|
|
expect(response.body.success).to.be.false;
|
|
});
|
|
|
|
it('should reject removal without required permissions', async () => {
|
|
const response = await request(app)
|
|
.delete('/tenant/logo')
|
|
.set('Host', 'test-tenant.example.com')
|
|
.set('Authorization', `Bearer ${testUserToken}`);
|
|
|
|
expect(response.status).to.equal(403);
|
|
expect(response.body.success).to.be.false;
|
|
});
|
|
|
|
it('should reject removal from user of different tenant', async () => {
|
|
const response = await request(app)
|
|
.delete('/tenant/logo')
|
|
.set('Host', 'test-tenant.example.com')
|
|
.set('Authorization', `Bearer ${otherTenantUserToken}`);
|
|
|
|
expect(response.status).to.equal(403);
|
|
expect(response.body.success).to.be.false;
|
|
expect(response.body.message).to.equal('Access denied: User not member of tenant');
|
|
|
|
// Verify security audit log
|
|
expect(securityLoggerStub.calledWith('warning', 'User attempted logo_removal on different tenant')).to.be.true;
|
|
});
|
|
|
|
it('should handle removal when no logo exists', async () => {
|
|
// Remove the logo first
|
|
await request(app)
|
|
.delete('/tenant/logo')
|
|
.set('Host', 'test-tenant.example.com')
|
|
.set('Authorization', `Bearer ${adminUserToken}`);
|
|
|
|
// Try to remove again
|
|
const response = await request(app)
|
|
.delete('/tenant/logo')
|
|
.set('Host', 'test-tenant.example.com')
|
|
.set('Authorization', `Bearer ${adminUserToken}`);
|
|
|
|
expect(response.status).to.equal(400);
|
|
expect(response.body.success).to.be.false;
|
|
expect(response.body.message).to.equal('No logo to remove');
|
|
|
|
// Verify warning audit log
|
|
expect(securityLoggerStub.calledWith('warning', 'Logo removal attempted when no logo exists')).to.be.true;
|
|
});
|
|
|
|
it('should validate tenant access and log appropriately', async () => {
|
|
const response = await request(app)
|
|
.delete('/tenant/logo')
|
|
.set('Host', 'test-tenant.example.com')
|
|
.set('Authorization', `Bearer ${adminUserToken}`);
|
|
|
|
expect(response.status).to.equal(200);
|
|
|
|
// Verify access validation log
|
|
expect(securityLoggerStub.calledWith('info', 'logo_removal access validated')).to.be.true;
|
|
});
|
|
});
|
|
|
|
describe('Security Validation Function', () => {
|
|
it('should log authentication failures', async () => {
|
|
const response = await request(app)
|
|
.post('/tenant/logo-upload')
|
|
.set('Host', 'test-tenant.example.com');
|
|
|
|
expect(response.status).to.equal(401);
|
|
|
|
// Note: This will be logged by the authenticateToken middleware
|
|
// but we can verify the endpoint behavior
|
|
});
|
|
|
|
it('should log tenant validation failures', async () => {
|
|
const response = await request(app)
|
|
.post('/tenant/logo-upload')
|
|
.set('Host', 'invalid-tenant.example.com')
|
|
.set('Authorization', `Bearer ${adminUserToken}`);
|
|
|
|
expect(response.status).to.equal(400);
|
|
expect(response.body.message).to.equal('Invalid tenant');
|
|
});
|
|
|
|
it('should log cross-tenant access attempts', async () => {
|
|
const response = await request(app)
|
|
.post('/tenant/logo-upload')
|
|
.set('Host', 'test-tenant.example.com')
|
|
.set('Authorization', `Bearer ${otherTenantUserToken}`);
|
|
|
|
expect(response.status).to.equal(403);
|
|
|
|
// Verify cross-tenant access was logged
|
|
expect(securityLoggerStub.calledWith('warning', 'User attempted logo_upload on different tenant')).to.be.true;
|
|
});
|
|
});
|
|
|
|
describe('Error Handling', () => {
|
|
it('should handle file system errors during upload', async () => {
|
|
// Mock fs to throw an error
|
|
const fsStub = sinon.stub(fs, 'unlinkSync').throws(new Error('File system error'));
|
|
|
|
try {
|
|
const testImagePath = path.join(__dirname, '../fixtures/test-logo.png');
|
|
const response = await request(app)
|
|
.post('/tenant/logo-upload')
|
|
.set('Host', 'test-tenant.example.com')
|
|
.set('Authorization', `Bearer ${adminUserToken}`)
|
|
.attach('logo', testImagePath);
|
|
|
|
// Should still succeed despite cleanup error
|
|
expect(response.status).to.equal(200);
|
|
} finally {
|
|
fsStub.restore();
|
|
}
|
|
});
|
|
|
|
it('should log errors appropriately', async () => {
|
|
// Mock models to throw an error
|
|
const modelStub = sinon.stub(models.Tenant, 'findOne').throws(new Error('Database error'));
|
|
|
|
try {
|
|
const testImagePath = path.join(__dirname, '../fixtures/test-logo.png');
|
|
const response = await request(app)
|
|
.post('/tenant/logo-upload')
|
|
.set('Host', 'test-tenant.example.com')
|
|
.set('Authorization', `Bearer ${adminUserToken}`)
|
|
.attach('logo', testImagePath);
|
|
|
|
expect(response.status).to.equal(500);
|
|
expect(response.body.message).to.equal('Failed to upload logo');
|
|
|
|
// Verify error was logged
|
|
expect(securityLoggerStub.calledWith('error', 'Logo upload failed with error')).to.be.true;
|
|
} finally {
|
|
modelStub.restore();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Audit Trail Verification', () => {
|
|
it('should create comprehensive audit trail for logo lifecycle', async () => {
|
|
const testImagePath = path.join(__dirname, '../fixtures/test-logo.png');
|
|
|
|
// Upload logo
|
|
const uploadResponse = await request(app)
|
|
.post('/tenant/logo-upload')
|
|
.set('Host', 'test-tenant.example.com')
|
|
.set('Authorization', `Bearer ${adminUserToken}`)
|
|
.attach('logo', testImagePath);
|
|
|
|
expect(uploadResponse.status).to.equal(200);
|
|
|
|
// Remove logo
|
|
const removeResponse = await request(app)
|
|
.delete('/tenant/logo')
|
|
.set('Host', 'test-tenant.example.com')
|
|
.set('Authorization', `Bearer ${adminUserToken}`);
|
|
|
|
expect(removeResponse.status).to.equal(200);
|
|
|
|
// Verify complete audit trail
|
|
const logCalls = securityLoggerStub.getCalls();
|
|
const logMessages = logCalls.map(call => call.args[1]);
|
|
|
|
expect(logMessages).to.include('logo_upload access validated');
|
|
expect(logMessages).to.include('Tenant logo uploaded successfully');
|
|
expect(logMessages).to.include('logo_removal access validated');
|
|
expect(logMessages).to.include('Logo file deleted from filesystem');
|
|
expect(logMessages).to.include('Tenant logo removed successfully');
|
|
});
|
|
|
|
it('should include security context in audit logs', async () => {
|
|
const testImagePath = path.join(__dirname, '../fixtures/test-logo.png');
|
|
|
|
const response = await request(app)
|
|
.post('/tenant/logo-upload')
|
|
.set('Host', 'test-tenant.example.com')
|
|
.set('Authorization', `Bearer ${adminUserToken}`)
|
|
.set('User-Agent', 'Test-Agent/1.0')
|
|
.attach('logo', testImagePath);
|
|
|
|
expect(response.status).to.equal(200);
|
|
|
|
// Find the successful upload log
|
|
const uploadLogCall = securityLoggerStub.getCalls().find(call =>
|
|
call.args[1] === 'Tenant logo uploaded successfully'
|
|
);
|
|
|
|
expect(uploadLogCall).to.exist;
|
|
expect(uploadLogCall.args[2]).to.include.keys(['userId', 'username', 'tenantId', 'tenantSlug', 'ip', 'userAgent']);
|
|
expect(uploadLogCall.args[2].username).to.equal('adminuser');
|
|
expect(uploadLogCall.args[2].userAgent).to.equal('Test-Agent/1.0');
|
|
});
|
|
});
|
|
}); |