Clean up test files and debug artifacts, add node_modules to gitignore, export DashboardManager for testing, and enhance pytest configuration with comprehensive test markers and settings
Tests / Backend Tests (Python) (3.10) (push) Has been cancelled
Tests / Backend Tests (Python) (3.11) (push) Has been cancelled
Tests / Backend Tests (Python) (3.12) (push) Has been cancelled
Tests / Frontend Tests (JS) (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / All Tests Passed (push) Has been cancelled
Tests / Backend Tests (Python) (3.10) (push) Has been cancelled
Tests / Backend Tests (Python) (3.11) (push) Has been cancelled
Tests / Backend Tests (Python) (3.12) (push) Has been cancelled
Tests / Frontend Tests (JS) (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / All Tests Passed (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,287 @@
|
||||
/**
|
||||
* DashboardCore - Core logic extracted from main.js for testability.
|
||||
*
|
||||
* This module contains the testable business logic:
|
||||
* - Authentication (login, logout, checkAuthStatus)
|
||||
* - API calls with auth headers
|
||||
* - WebSocket message handling
|
||||
* - Data management (hosts, tasks, schedules)
|
||||
*
|
||||
* The main.js file imports this module and adds DOM-specific functionality.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Core dashboard functionality that can be tested without DOM dependencies.
|
||||
*/
|
||||
export class DashboardCore {
|
||||
constructor(options = {}) {
|
||||
// Allow injection of dependencies for testing
|
||||
this.apiBase = options.apiBase || (typeof window !== 'undefined' ? window.location.origin : 'http://localhost');
|
||||
this.storage = options.storage || (typeof localStorage !== 'undefined' ? localStorage : null);
|
||||
this.fetchFn = options.fetch || (typeof fetch !== 'undefined' ? fetch : null);
|
||||
this.WebSocketClass = options.WebSocket || (typeof WebSocket !== 'undefined' ? WebSocket : null);
|
||||
|
||||
// Authentication state
|
||||
this.accessToken = this.storage?.getItem('accessToken') || null;
|
||||
this.currentUser = null;
|
||||
this.setupRequired = false;
|
||||
|
||||
// Data state
|
||||
this.hosts = [];
|
||||
this.tasks = [];
|
||||
this.schedules = [];
|
||||
this.logs = [];
|
||||
this.alerts = [];
|
||||
this.alertsUnread = 0;
|
||||
|
||||
// WebSocket
|
||||
this.ws = null;
|
||||
|
||||
// Callbacks for UI updates (set by main.js)
|
||||
this.onHostsUpdated = null;
|
||||
this.onTasksUpdated = null;
|
||||
this.onSchedulesUpdated = null;
|
||||
this.onAlertsUpdated = null;
|
||||
this.onAuthStateChanged = null;
|
||||
this.onNotification = null;
|
||||
}
|
||||
|
||||
// ===== AUTHENTICATION =====
|
||||
|
||||
getAuthHeaders() {
|
||||
const headers = { 'Content-Type': 'application/json' };
|
||||
if (this.accessToken) {
|
||||
headers['Authorization'] = `Bearer ${this.accessToken}`;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
async checkAuthStatus() {
|
||||
try {
|
||||
const response = await this.fetchFn(`${this.apiBase}/api/auth/status`, {
|
||||
headers: this.getAuthHeaders()
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
this.setupRequired = data.setup_required;
|
||||
|
||||
if (data.setup_required) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (data.authenticated && data.user) {
|
||||
this.currentUser = data.user;
|
||||
this.onAuthStateChanged?.({ authenticated: true, user: data.user });
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('Auth status check failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async login(username, password) {
|
||||
try {
|
||||
const response = await this.fetchFn(`${this.apiBase}/api/auth/login/json`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.detail || 'Login failed');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
this.accessToken = data.access_token;
|
||||
this.storage?.setItem('accessToken', data.access_token);
|
||||
|
||||
await this.checkAuthStatus();
|
||||
this.onNotification?.('Connexion réussie', 'success');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Login failed:', error);
|
||||
this.onNotification?.(error.message, 'error');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
logout() {
|
||||
this.accessToken = null;
|
||||
this.currentUser = null;
|
||||
this.storage?.removeItem('accessToken');
|
||||
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
this.ws = null;
|
||||
}
|
||||
|
||||
this.onAuthStateChanged?.({ authenticated: false, user: null });
|
||||
this.onNotification?.('Déconnexion réussie', 'success');
|
||||
}
|
||||
|
||||
// ===== API CALLS =====
|
||||
|
||||
async apiCall(endpoint, options = {}) {
|
||||
const url = `${this.apiBase}${endpoint}`;
|
||||
const defaultOptions = {
|
||||
headers: this.getAuthHeaders()
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await this.fetchFn(url, { ...defaultOptions, ...options });
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error(`API Error (${endpoint}):`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== WEBSOCKET =====
|
||||
|
||||
connectWebSocket(wsUrl = null) {
|
||||
if (!this.WebSocketClass) {
|
||||
console.warn('WebSocket not available');
|
||||
return;
|
||||
}
|
||||
|
||||
const url = wsUrl || this._getDefaultWsUrl();
|
||||
|
||||
try {
|
||||
this.ws = new this.WebSocketClass(url);
|
||||
|
||||
this.ws.onopen = () => {
|
||||
console.log('WebSocket connected');
|
||||
};
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
this.handleWebSocketMessage(data);
|
||||
} catch (e) {
|
||||
console.error('WebSocket message parse error:', e);
|
||||
}
|
||||
};
|
||||
|
||||
this.ws.onclose = () => {
|
||||
console.log('WebSocket closed');
|
||||
};
|
||||
|
||||
this.ws.onerror = (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('WebSocket connection failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
_getDefaultWsUrl() {
|
||||
if (typeof window === 'undefined') {
|
||||
return 'ws://localhost/ws';
|
||||
}
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
return `${protocol}//${window.location.host}/ws`;
|
||||
}
|
||||
|
||||
handleWebSocketMessage(message) {
|
||||
const { type, data } = message;
|
||||
|
||||
switch (type) {
|
||||
case 'host_updated':
|
||||
this.updateHostInList(data);
|
||||
break;
|
||||
case 'task_started':
|
||||
case 'task_updated':
|
||||
case 'task_completed':
|
||||
this.handleTaskUpdate(data);
|
||||
break;
|
||||
case 'schedule_completed':
|
||||
this.updateScheduleStatus(data);
|
||||
break;
|
||||
case 'alert_created':
|
||||
this.handleAlertCreated(data);
|
||||
break;
|
||||
case 'hosts_synced':
|
||||
this.onHostsUpdated?.();
|
||||
break;
|
||||
default:
|
||||
// Unknown message type - ignore
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== DATA MANAGEMENT =====
|
||||
|
||||
updateHostInList(hostData) {
|
||||
if (!hostData?.id) return;
|
||||
|
||||
const idx = this.hosts.findIndex(h => h.id === hostData.id);
|
||||
if (idx >= 0) {
|
||||
this.hosts[idx] = { ...this.hosts[idx], ...hostData };
|
||||
} else {
|
||||
this.hosts.push(hostData);
|
||||
}
|
||||
this.onHostsUpdated?.();
|
||||
}
|
||||
|
||||
handleTaskUpdate(taskData) {
|
||||
if (!taskData?.id) return;
|
||||
|
||||
const idx = this.tasks.findIndex(t => t.id === taskData.id);
|
||||
if (idx >= 0) {
|
||||
this.tasks[idx] = { ...this.tasks[idx], ...taskData };
|
||||
} else {
|
||||
this.tasks.unshift(taskData);
|
||||
}
|
||||
this.onTasksUpdated?.();
|
||||
}
|
||||
|
||||
updateScheduleStatus(scheduleData) {
|
||||
if (!scheduleData?.schedule_id) return;
|
||||
|
||||
const idx = this.schedules.findIndex(s => s.id === scheduleData.schedule_id);
|
||||
if (idx >= 0) {
|
||||
this.schedules[idx].last_status = scheduleData.status;
|
||||
this.schedules[idx].last_run_at = scheduleData.completed_at;
|
||||
}
|
||||
this.onSchedulesUpdated?.();
|
||||
}
|
||||
|
||||
handleAlertCreated(alert) {
|
||||
if (!alert) return;
|
||||
this.alerts = [alert, ...this.alerts].slice(0, 200);
|
||||
this.alertsUnread++;
|
||||
this.onAlertsUpdated?.();
|
||||
}
|
||||
|
||||
// ===== UTILITY =====
|
||||
|
||||
escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
const div = typeof document !== 'undefined' ? document.createElement('div') : null;
|
||||
if (div) {
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
// Fallback for non-DOM environments
|
||||
return String(text)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
}
|
||||
|
||||
// Export for both ESM and CommonJS
|
||||
export default DashboardCore;
|
||||
+7
-1
@@ -1,4 +1,5 @@
|
||||
// Homelab Dashboard JavaScript - Intégration API
|
||||
|
||||
class DashboardManager {
|
||||
constructor() {
|
||||
// Configuration API - JWT token stored in localStorage
|
||||
@@ -9583,4 +9584,9 @@ window.showCreateScheduleModal = function(prefilledPlaybook = null) {
|
||||
} catch (e) {
|
||||
console.error('showCreateScheduleModal fallback error:', e);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Export for testing (ESM/CommonJS compatible)
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = { DashboardManager };
|
||||
}
|
||||
Reference in New Issue
Block a user