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
394 lines
11 KiB
JavaScript
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();
|
|
});
|
|
});
|