Files
drone-detector/server/tests/routes/tenant-logo.test.js
2025-09-20 07:32:22 +02:00

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');
});
});
});