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