Add comprehensive unit tests for Security, UserModel, and Validation utilities

- Implemented SecurityTest to validate token generation, CSRF protection, input sanitization, and rate limiting.
- Created UserModelTest to ensure correct database operations for user management, including creation, updating, banning, and fetching active users.
- Developed ValidationTest to verify input validation and sanitization for user IDs, nicknames, messages, and API requests.
- Introduced Security and Validation utility classes with methods for secure token generation, input sanitization, and comprehensive validation rules.
This commit is contained in:
Vincent 2025-09-30 21:22:28 -04:00
parent 5692874b10
commit 41cd7a4fd8
32 changed files with 5796 additions and 368 deletions

View file

@ -65,8 +65,8 @@
UIControls.DOMUtils.addEvent(sendButton, 'click', sendMessage);
}
// Start polling for messages
startMessagePolling();
// Start real-time SSE connection for chat
startSSEConnection();
// Send initial heartbeat
API.sendHeartbeat();
@ -306,8 +306,132 @@
});
}
// Message polling system
// SSE (Server-Sent Events) for real-time communication
let eventSource = null;
let sseReconnectAttempts = 0;
const maxSSEReconnectAttempts = 5;
function startSSEConnection() {
if (typeof(EventSource) === 'undefined') {
// Fallback to polling if SSE not supported
AppLogger.warn('EventSource not supported, falling back to polling');
startMessagePolling();
return;
}
if (eventSource) {
eventSource.close();
}
const userId = AppState.userId;
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '';
// Build SSE URL with CSRF protection and last message ID
const sseUrl = `?sse=1&user_id=${encodeURIComponent(userId)}&csrf=${encodeURIComponent(csrfToken)}&last_id=${encodeURIComponent(AppState.lastMessageId)}`;
eventSource = new EventSource(sseUrl);
// Connection established
eventSource.addEventListener('connection', function(event) {
const data = JSON.parse(event.data);
AppLogger.log('SSE connected:', data);
sseReconnectAttempts = 0;
updateConnectionStatus(true);
ScreenReader.connectionStatus('connected');
});
// New messages arrived
eventSource.addEventListener('new_messages', function(event) {
const messageData = JSON.parse(event.data);
const newMessages = messageData.data.messages;
if (newMessages && newMessages.length > 0) {
// Process each new message
newMessages.forEach(msg => {
// Skip if it's our own message (might be received via SSE faster than AJAX response)
if (msg.user_id === AppState.userId) {
return;
}
appendMessage(msg);
AppState.allMessages.push(msg);
// Show notification if chat is collapsed
if (AppState.chatCollapsed) {
AppState.unreadCount++;
UIControls.updateNotificationBadge();
playNotificationSound();
}
});
// Update last message ID
const lastMessage = newMessages[newMessages.length - 1];
if (lastMessage && lastMessage.id) {
AppState.lastMessageId = lastMessage.id;
}
// Announce new messages for screen readers
if (newMessages.length === 1) {
ScreenReader.messageReceived('New message received');
} else {
ScreenReader.messageGroup(newMessages.map(msg => ({
nickname: msg.nickname,
message: msg.message
})), 'added');
}
}
});
// Viewer count updates
eventSource.addEventListener('viewer_count_update', function(event) {
const viewerData = JSON.parse(event.data);
const count = viewerData.data.count;
updateViewerCount(count);
ScreenReader.viewerCount(count);
});
// Heartbeat for connection monitoring
eventSource.addEventListener('heartbeat', function(event) {
const heartbeatData = JSON.parse(event.data);
// Connection is alive, no action needed
});
// Connection errors
eventSource.addEventListener('error', function(event) {
AppLogger.warn('SSE connection error:', event);
updateConnectionStatus(false);
ScreenReader.connectionStatus('error', 'Connection lost, attempting to reconnect');
// Auto-reconnect with backoff
if (sseReconnectAttempts < maxSSEReconnectAttempts) {
sseReconnectAttempts++;
const delay = Math.min(1000 * Math.pow(2, sseReconnectAttempts), 30000);
setTimeout(() => {
AppLogger.log(`Attempting SSE reconnection (${sseReconnectAttempts}/${maxSSEReconnectAttempts})`);
startSSEConnection();
}, delay);
} else {
// Fall back to polling after max attempts
AppLogger.error('SSE maximum reconnection attempts reached, falling back to polling');
startMessagePolling();
}
});
// Connection closed
eventSource.addEventListener('disconnect', function(event) {
const disconnectData = JSON.parse(event.data);
AppLogger.log('SSE disconnected:', disconnectData.data.reason);
});
}
// Traditional polling system (fallback)
function startMessagePolling() {
AppLogger.log('Starting message polling (fallback)');
// Poll for new messages
setInterval(fetchMessages, AppConfig.api.chatPollInterval);
@ -515,6 +639,29 @@
AppState.lastMessageId = '';
}
// Update connection status functions (from UI controls)
function updateConnectionStatus(online) {
const statusDot = document.getElementById('statusDot');
const statusText = document.getElementById('statusText');
if (statusDot && statusText) {
if (online) {
statusDot.classList.remove('offline');
statusText.textContent = 'Connected';
} else {
statusDot.classList.add('offline');
statusText.textContent = 'Reconnecting...';
}
}
}
function updateViewerCount(count) {
const viewerElement = document.getElementById('viewerCount');
if (viewerElement) {
viewerElement.textContent = count + (count === 1 ? ' viewer' : ' viewers');
}
}
// Utility function for HTML escaping
function escapeHtml(text) {
const div = document.createElement('div');