Fix jwt-token
This commit is contained in:
1
server/tests/fixtures/test-logo.png
vendored
Normal file
1
server/tests/fixtures/test-logo.png
vendored
Normal file
@@ -0,0 +1 @@
|
||||
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
|
||||
453
server/tests/routes/tenant-logo.test.js
Normal file
453
server/tests/routes/tenant-logo.test.js
Normal file
@@ -0,0 +1,453 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user