Project-Image-Uploader/backend/tests/unit/middlewares/hostGate.test.js
matthias.lotz e4ddd229b8 feat: Public/Internal Host Separation
Implemented subdomain-based feature separation for production deployment.

**Backend:**
- New hostGate middleware for host-based API protection
- Public host blocks: /api/admin, /api/groups, /api/slideshow, /api/auth
- Public host allows: /api/upload, /api/manage, /api/social-media/platforms
- Rate limiting: 20 uploads/hour on public host (publicUploadLimiter)
- Audit log enhancement: source_host, source_type tracking
- Database migration 009: Added source tracking columns

**Frontend:**
- Host detection utility (hostDetection.js) with feature flags
- React code splitting with lazy loading for internal features
- Conditional routing: Internal routes only mounted on internal host
- 404 page: Host-specific messaging and navbar
- Clipboard fallback for HTTP environments

**Configuration:**
- Environment variables: PUBLIC_HOST, INTERNAL_HOST, ENABLE_HOST_RESTRICTION
- Docker dev setup: HOST variables, TRUST_PROXY_HOPS configuration
- Frontend .env.development: DANGEROUSLY_DISABLE_HOST_CHECK for Webpack

**Testing:**
- 20/20 hostGate unit tests passing
- Local testing guide in README.dev.md
- /etc/hosts setup for public.test.local, internal.test.local

**Bug Fixes:**
- Fixed clipboard API not available on HTTP
- Fixed missing PUBLIC_HOST in frontend env-config.js
- Fixed wrong navbar on 404 page for public host
- Fixed social media platforms loading in UUID management

**Documentation:**
- CHANGELOG.md: Complete feature documentation
- README.md: Feature overview
- README.dev.md: Host-separation testing guide
- TESTING-HOST-SEPARATION.md: Integration note
2025-11-25 22:02:53 +01:00

268 lines
9.3 KiB
JavaScript

/**
* Unit Tests für hostGate Middleware
* Testet Host-basierte Zugriffskontrolle
*/
// Setup ENV VOR dem Require
process.env.ENABLE_HOST_RESTRICTION = 'true';
process.env.PUBLIC_HOST = 'public.example.com';
process.env.INTERNAL_HOST = 'internal.example.com';
process.env.NODE_ENV = 'development';
let hostGate;
// Helper to create mock request with headers
const createMockRequest = (hostname, path = '/') => {
return {
path,
get: (headerName) => {
if (headerName.toLowerCase() === 'x-forwarded-host') {
return hostname;
}
if (headerName.toLowerCase() === 'host') {
return hostname;
}
return null;
}
};
};
describe('Host Gate Middleware', () => {
let req, res, next;
let originalEnv;
beforeAll(() => {
// Sichere Original-Env
originalEnv = { ...process.env };
// Lade Modul NACH ENV setup
hostGate = require('../../../src/middlewares/hostGate');
});
beforeEach(() => {
// Mock response object
res = {
status: jest.fn().mockReturnThis(),
json: jest.fn()
};
// Mock next function
next = jest.fn();
// Reset req for each test
req = null;
// Setup Environment
process.env.ENABLE_HOST_RESTRICTION = 'true';
process.env.PUBLIC_HOST = 'public.example.com';
process.env.INTERNAL_HOST = 'internal.example.com';
process.env.NODE_ENV = 'development'; // NOT 'test' to enable restrictions
});
afterEach(() => {
jest.clearAllMocks();
});
afterAll(() => {
// Restore Original-Env
process.env = originalEnv;
});
describe('Host Detection', () => {
test('should detect public host from X-Forwarded-Host header', () => {
req = createMockRequest('public.example.com');
hostGate(req, res, next);
expect(req.isPublicHost).toBe(true);
expect(req.isInternalHost).toBe(false);
expect(req.requestSource).toBe('public');
});
test('should detect internal host from X-Forwarded-Host header', () => {
req = createMockRequest('internal.example.com');
hostGate(req, res, next);
expect(req.isPublicHost).toBe(false);
expect(req.isInternalHost).toBe(true);
expect(req.requestSource).toBe('internal');
});
test('should fallback to Host header if X-Forwarded-Host not present', () => {
req = createMockRequest('public.example.com');
hostGate(req, res, next);
expect(req.isPublicHost).toBe(true);
});
test('should handle localhost as internal host', () => {
req = createMockRequest('localhost:3000');
hostGate(req, res, next);
expect(req.isInternalHost).toBe(true);
expect(req.isPublicHost).toBe(false);
});
test('should strip port from hostname', () => {
req = createMockRequest('public.example.com:8080');
hostGate(req, res, next);
expect(req.isPublicHost).toBe(true);
});
});
describe('Route Protection', () => {
test('should block admin routes on public host', () => {
req = createMockRequest('public.example.com', '/api/admin/deletion-log');
hostGate(req, res, next);
expect(res.status).toHaveBeenCalledWith(403);
expect(res.json).toHaveBeenCalledWith({
error: 'Not available on public host',
message: 'This endpoint is only available on the internal network'
});
expect(next).not.toHaveBeenCalled();
});
test('should block groups routes on public host', () => {
req = createMockRequest('public.example.com', '/api/groups');
hostGate(req, res, next);
expect(res.status).toHaveBeenCalledWith(403);
});
test('should block slideshow routes on public host', () => {
req = createMockRequest('public.example.com', '/api/slideshow');
hostGate(req, res, next);
expect(res.status).toHaveBeenCalledWith(403);
});
test('should block migration routes on public host', () => {
req = createMockRequest('public.example.com', '/api/migration/start');
hostGate(req, res, next);
expect(res.status).toHaveBeenCalledWith(403);
});
test('should block auth login on public host', () => {
req = createMockRequest('public.example.com', '/api/auth/login');
hostGate(req, res, next);
expect(res.status).toHaveBeenCalledWith(403);
});
});
describe('Allowed Routes', () => {
test('should allow upload route on public host', () => {
req = createMockRequest('public.example.com', '/api/upload');
hostGate(req, res, next);
expect(next).toHaveBeenCalled();
expect(res.status).not.toHaveBeenCalled();
});
test('should allow manage routes on public host', () => {
req = createMockRequest('public.example.com', '/api/manage/abc-123');
hostGate(req, res, next);
expect(next).toHaveBeenCalled();
});
test('should allow preview routes on public host', () => {
req = createMockRequest('public.example.com', '/api/previews/image.jpg');
hostGate(req, res, next);
expect(next).toHaveBeenCalled();
});
test('should allow consent routes on public host', () => {
req = createMockRequest('public.example.com', '/api/consent');
hostGate(req, res, next);
expect(next).toHaveBeenCalled();
});
test('should allow all routes on internal host', () => {
req = createMockRequest('internal.example.com', '/api/admin/deletion-log');
hostGate(req, res, next);
expect(next).toHaveBeenCalled();
expect(res.status).not.toHaveBeenCalled();
});
});
describe('Feature Flags', () => {
test('should bypass restriction when NODE_ENV is test and not explicitly enabled', () => {
// Reload module with test environment
delete require.cache[require.resolve('../../../src/middlewares/hostGate')];
process.env.NODE_ENV = 'test';
process.env.ENABLE_HOST_RESTRICTION = 'false'; // Explicitly disabled
const hostGateTest = require('../../../src/middlewares/hostGate');
req = createMockRequest('public.example.com', '/api/admin/test');
hostGateTest(req, res, next);
expect(next).toHaveBeenCalled();
expect(res.status).not.toHaveBeenCalled();
expect(req.isInternalHost).toBe(true);
// Restore
delete require.cache[require.resolve('../../../src/middlewares/hostGate')];
process.env.NODE_ENV = 'development';
process.env.ENABLE_HOST_RESTRICTION = 'true';
});
test('should work in test environment when explicitly enabled', () => {
// Reload module with test environment BUT explicitly enabled
delete require.cache[require.resolve('../../../src/middlewares/hostGate')];
process.env.NODE_ENV = 'test';
process.env.ENABLE_HOST_RESTRICTION = 'true'; // Explicitly enabled
const hostGateTest = require('../../../src/middlewares/hostGate');
req = createMockRequest('public.example.com', '/api/admin/test');
hostGateTest(req, res, next);
// Should block because explicitly enabled
expect(res.status).toHaveBeenCalledWith(403);
expect(next).not.toHaveBeenCalled();
// Restore
delete require.cache[require.resolve('../../../src/middlewares/hostGate')];
process.env.NODE_ENV = 'development';
process.env.ENABLE_HOST_RESTRICTION = 'true';
});
});
describe('Request Source Tracking', () => {
test('should set requestSource to "public" for public host', () => {
req = createMockRequest('public.example.com', '/api/upload');
hostGate(req, res, next);
expect(req.requestSource).toBe('public');
});
test('should set requestSource to "internal" for internal host', () => {
req = createMockRequest('internal.example.com', '/api/admin/test');
hostGate(req, res, next);
expect(req.requestSource).toBe('internal');
});
test('should set requestSource to "internal" when restrictions disabled', () => {
// Reload module with disabled restriction
delete require.cache[require.resolve('../../../src/middlewares/hostGate')];
process.env.ENABLE_HOST_RESTRICTION = 'false';
const hostGateDisabled = require('../../../src/middlewares/hostGate');
req = createMockRequest('anything.example.com', '/api/test');
hostGateDisabled(req, res, next);
expect(req.requestSource).toBe('internal');
// Restore
delete require.cache[require.resolve('../../../src/middlewares/hostGate')];
process.env.ENABLE_HOST_RESTRICTION = 'true';
});
});
});