Bruno Charest ecefbc8611
Some checks failed
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
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
2025-12-15 08:15:49 -05:00

394 lines
11 KiB
JavaScript

/**
* Tests for main.js - DashboardManager class.
*
* Covers:
* - Authentication flow (login, logout, checkAuthStatus)
* - API calls with auth headers
* - WebSocket connection
* - Data loading and state management
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import {
setupFetchMock,
createMockResponse,
setupMinimalDOM,
waitForDOM,
MockWebSocket,
localStorageMock
} from './setup.js';
// ============================================================================
// TestDashboardManager - Mock class for testing
// ============================================================================
// Note: main.js is a browser script with side effects (auto-instantiation on DOMContentLoaded).
// We test the logic using this mock class that mirrors the real implementation.
// This ensures tests are fast and don't depend on DOM state.
class TestDashboardManager {
constructor() {
this.apiBase = 'http://localhost';
this.accessToken = localStorage.getItem('accessToken') || null;
this.currentUser = null;
this.setupRequired = false;
this.hosts = [];
this.tasks = [];
this.schedules = [];
this.ws = null;
}
getAuthHeaders() {
const headers = { 'Content-Type': 'application/json' };
if (this.accessToken) {
headers['Authorization'] = `Bearer ${this.accessToken}`;
}
return headers;
}
async checkAuthStatus() {
try {
const response = await fetch(`${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;
return true;
}
return false;
} catch {
return false;
}
}
async login(username, password) {
try {
const response = await fetch(`${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;
localStorage.setItem('accessToken', data.access_token);
await this.checkAuthStatus();
return true;
} catch (error) {
console.error('Login failed:', error);
return false;
}
}
logout() {
this.accessToken = null;
this.currentUser = null;
localStorage.removeItem('accessToken');
if (this.ws) {
this.ws.close();
this.ws = null;
}
}
connectWebSocket() {
const protocol = this.apiBase.startsWith('https') ? 'wss' : 'ws';
const wsUrl = `${protocol}://localhost/ws`;
this.ws = new WebSocket(wsUrl);
this.ws.onopen = () => {
console.log('WebSocket connected');
};
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
this.handleWebSocketMessage(data);
};
this.ws.onclose = () => {
console.log('WebSocket closed');
};
}
handleWebSocketMessage(message) {
switch (message.type) {
case 'host_updated':
this.updateHostInList(message.data);
break;
case 'task_started':
this.addTaskToList(message.data);
break;
case 'schedule_completed':
this.updateScheduleStatus(message.data);
break;
}
}
updateHostInList(hostData) {
const idx = this.hosts.findIndex(h => h.id === hostData.id);
if (idx >= 0) {
this.hosts[idx] = { ...this.hosts[idx], ...hostData };
}
}
addTaskToList(taskData) {
this.tasks.unshift(taskData);
}
updateScheduleStatus(scheduleData) {
const idx = this.schedules.findIndex(s => s.id === scheduleData.schedule_id);
if (idx >= 0) {
this.schedules[idx].last_status = scheduleData.status;
}
}
}
// ============================================================================
// TESTS: Authentication
// ============================================================================
describe('DashboardManager - Authentication', () => {
let manager;
beforeEach(() => {
setupFetchMock();
manager = new TestDashboardManager();
});
describe('getAuthHeaders', () => {
it('returns headers without auth when no token', () => {
manager.accessToken = null;
const headers = manager.getAuthHeaders();
expect(headers['Content-Type']).toBe('application/json');
expect(headers['Authorization']).toBeUndefined();
});
it('returns headers with Bearer token when authenticated', () => {
manager.accessToken = 'test-token-123';
const headers = manager.getAuthHeaders();
expect(headers['Authorization']).toBe('Bearer test-token-123');
});
});
describe('checkAuthStatus', () => {
it('returns true when authenticated', async () => {
setupFetchMock({
'/api/auth/status': {
setup_required: false,
authenticated: true,
user: { username: 'testuser', role: 'admin' }
}
});
const result = await manager.checkAuthStatus();
expect(result).toBe(true);
expect(manager.currentUser).toEqual({ username: 'testuser', role: 'admin' });
});
it('returns false when setup required', async () => {
setupFetchMock({
'/api/auth/status': {
setup_required: true,
authenticated: false,
user: null
}
});
const result = await manager.checkAuthStatus();
expect(result).toBe(false);
expect(manager.setupRequired).toBe(true);
});
it('returns false when not authenticated', async () => {
setupFetchMock({
'/api/auth/status': {
setup_required: false,
authenticated: false,
user: null
}
});
const result = await manager.checkAuthStatus();
expect(result).toBe(false);
expect(manager.currentUser).toBeNull();
});
it('returns false on network error', async () => {
global.fetch = vi.fn().mockRejectedValue(new Error('Network error'));
const result = await manager.checkAuthStatus();
expect(result).toBe(false);
});
});
describe('login', () => {
it('stores token on successful login', async () => {
setupFetchMock({
'/api/auth/login/json': {
access_token: 'new-jwt-token',
token_type: 'bearer',
expires_in: 86400
},
'/api/auth/status': {
setup_required: false,
authenticated: true,
user: { username: 'testuser' }
}
});
const result = await manager.login('testuser', 'password123');
expect(result).toBe(true);
expect(manager.accessToken).toBe('new-jwt-token');
expect(localStorageMock.setItem).toHaveBeenCalledWith('accessToken', 'new-jwt-token');
});
it('returns false on invalid credentials', async () => {
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
global.fetch = vi.fn().mockResolvedValue(
createMockResponse({ detail: 'Invalid credentials' }, 401)
);
const result = await manager.login('wrong', 'credentials');
expect(result).toBe(false);
expect(manager.accessToken).toBeNull();
consoleSpy.mockRestore();
});
});
describe('logout', () => {
it('clears token and user data', () => {
manager.accessToken = 'some-token';
manager.currentUser = { username: 'test' };
manager.logout();
expect(manager.accessToken).toBeNull();
expect(manager.currentUser).toBeNull();
expect(localStorageMock.removeItem).toHaveBeenCalledWith('accessToken');
});
it('closes WebSocket connection', () => {
manager.ws = new MockWebSocket('ws://localhost/ws');
manager.ws.readyState = MockWebSocket.OPEN;
const closeSpy = vi.spyOn(manager.ws, 'close');
manager.logout();
expect(closeSpy).toHaveBeenCalled();
});
});
});
// ============================================================================
// TESTS: WebSocket
// ============================================================================
describe('DashboardManager - WebSocket', () => {
let manager;
beforeEach(() => {
manager = new TestDashboardManager();
});
describe('connectWebSocket', () => {
it('creates WebSocket connection', () => {
manager.connectWebSocket();
expect(manager.ws).toBeInstanceOf(MockWebSocket);
expect(manager.ws.url).toBe('ws://localhost/ws');
});
it('sets up event handlers', async () => {
manager.connectWebSocket();
await waitForDOM(10);
expect(manager.ws.onopen).toBeDefined();
expect(manager.ws.onmessage).toBeDefined();
expect(manager.ws.onclose).toBeDefined();
});
});
describe('handleWebSocketMessage', () => {
beforeEach(() => {
manager.hosts = [{ id: 'host-1', name: 'server1', status: 'unknown' }];
manager.tasks = [];
manager.schedules = [{ id: 'sched-1', name: 'Daily Backup', last_status: 'pending' }];
});
it('handles host_updated event', () => {
manager.handleWebSocketMessage({
type: 'host_updated',
data: { id: 'host-1', status: 'online' }
});
expect(manager.hosts[0].status).toBe('online');
});
it('handles task_started event', () => {
manager.handleWebSocketMessage({
type: 'task_started',
data: { id: 'task-1', playbook: 'test.yml' }
});
expect(manager.tasks.length).toBe(1);
expect(manager.tasks[0].id).toBe('task-1');
});
it('handles schedule_completed event', () => {
manager.handleWebSocketMessage({
type: 'schedule_completed',
data: { schedule_id: 'sched-1', status: 'success' }
});
expect(manager.schedules[0].last_status).toBe('success');
});
});
});
// ============================================================================
// TESTS: Token Persistence
// ============================================================================
describe('DashboardManager - Token Persistence', () => {
it('loads token from localStorage on init', () => {
localStorageMock.setItem('accessToken', 'stored-token');
const manager = new TestDashboardManager();
expect(manager.accessToken).toBe('stored-token');
});
it('starts with null token if localStorage empty', () => {
localStorageMock.clear();
const manager = new TestDashboardManager();
expect(manager.accessToken).toBeNull();
});
});