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:
parent
5692874b10
commit
41cd7a4fd8
32 changed files with 5796 additions and 368 deletions
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -680,48 +680,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Initialize event listeners
|
||||
function initializeEventListeners() {
|
||||
// Keyboard shortcuts
|
||||
DOMUtils.addEvent(document, 'keydown', handleKeyboardShortcuts);
|
||||
|
||||
// Window resize for mobile responsiveness
|
||||
DOMUtils.addEvent(window, 'resize', handleWindowResize);
|
||||
|
||||
// Page visibility for notification clearing
|
||||
DOMUtils.addEvent(document, 'visibilitychange', handleVisibilityChange);
|
||||
|
||||
// Touch gesture support for mobile
|
||||
const videoSection = document.getElementById('videoSection');
|
||||
if (videoSection) {
|
||||
// Swipe gestures on video area for chat toggle
|
||||
DOMUtils.addEvent(videoSection, 'touchstart', handleTouchStart, { passive: true });
|
||||
DOMUtils.addEvent(videoSection, 'touchmove', handleTouchMove, { passive: true });
|
||||
DOMUtils.addEvent(videoSection, 'touchend', handleTouchEnd, { passive: true });
|
||||
|
||||
// Double-tap on video for fullscreen
|
||||
DOMUtils.addEvent(videoSection, 'touchend', handleVideoDoubleTap);
|
||||
}
|
||||
|
||||
// Pull-to-refresh on the whole document (only on mobile)
|
||||
DOMUtils.addEvent(document, 'touchstart', handlePullToRefreshTouchStart, { passive: true });
|
||||
DOMUtils.addEvent(document, 'touchmove', handlePullToRefreshTouchMove, { passive: false });
|
||||
DOMUtils.addEvent(document, 'touchend', handlePullToRefreshTouchEnd, { passive: true });
|
||||
|
||||
// Mobile navigation buttons
|
||||
const mobileNav = document.getElementById('mobileNav');
|
||||
if (mobileNav) {
|
||||
mobileNav.addEventListener('click', function(event) {
|
||||
const button = event.target.closest('.mobile-nav-btn');
|
||||
if (button && button.dataset.action) {
|
||||
event.preventDefault();
|
||||
handleMobileNavAction(button.dataset.action);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
AppLogger.log('UI controls event listeners initialized');
|
||||
}
|
||||
|
||||
// HTML escape utility function
|
||||
function escapeHtml(text) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue