homelab_automation/tests/frontend/storage_details.test.js
Bruno Charest 6d8432169b
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
Add enhanced terminal history panel UI with animations, keyboard navigation, advanced filtering, search highlighting, and improved storage metrics display with detailed filesystem tables and ZFS/LVM support
2025-12-21 12:31:08 -05:00

430 lines
15 KiB
JavaScript

/**
* Tests for the Storage Details UI component.
*
* Tests cover:
* - Rendering of storage details accordion
* - Accordion toggle behavior
* - Filesystem table rendering with warning thresholds
* - ZFS and LVM section rendering
* - Inspector mode drawer
* - Feature flag badges (ZFS, LVM)
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
// Mock storage_details data fixtures
const mockLinuxDebianStorageDetails = {
os_type: 'linux-debian',
status: 'ok',
collected_at: '2025-12-19T10:00:00Z',
commands_run: [
{ cmd: 'lsblk', status: 'ok' },
{ cmd: 'findmnt', status: 'ok' },
{ cmd: 'df', status: 'ok' }
],
partial_failures: [],
summary: {
total_bytes: 500107862016,
used_bytes: 125026965504,
free_bytes: 349600112640,
used_pct: 25.0
},
filesystems: [
{
device: '/dev/sda2',
fstype: 'ext4',
size_bytes: 499570991104,
used_bytes: 125026965504,
avail_bytes: 349600112640,
use_pct: 26,
mountpoint: '/'
}
],
block_devices: [
{
name: 'sda',
type: 'disk',
size: 500107862016,
model: 'Samsung SSD 860'
}
],
lvm: { pvs: [], vgs: [], lvs: [] },
zfs: { pools: [], datasets: [] },
feature_flags: {
has_lvm: false,
has_zfs: false,
has_lsblk: true,
has_findmnt: true
}
};
const mockTrueNASStorageDetails = {
os_type: 'truenas-core',
status: 'ok',
collected_at: '2025-12-19T10:00:00Z',
commands_run: [
{ cmd: 'df', status: 'ok' },
{ cmd: 'zpool list', status: 'ok' },
{ cmd: 'zfs list', status: 'ok' }
],
partial_failures: [],
summary: {
total_bytes: 4000787030016,
used_bytes: 2800550921011,
free_bytes: 1200236109005,
used_pct: 70.0
},
filesystems: [],
block_devices: [],
lvm: { pvs: [], vgs: [], lvs: [] },
zfs: {
pools: [
{
name: 'tank',
size_bytes: 4000787030016,
alloc_bytes: 2800550921011,
free_bytes: 1200236109005,
cap_pct: 70,
health: 'ONLINE'
}
],
datasets: [
{
name: 'tank/apps',
used_bytes: 107374182400,
avail_bytes: 1200236109005,
mountpoint: '/mnt/tank/apps',
type: 'filesystem'
}
]
},
feature_flags: {
has_lvm: false,
has_zfs: true,
has_lsblk: false,
has_findmnt: false
}
};
const mockHighUsageStorageDetails = {
os_type: 'linux-debian',
status: 'ok',
collected_at: '2025-12-19T10:00:00Z',
commands_run: [{ cmd: 'df', status: 'ok' }],
partial_failures: [],
summary: {
total_bytes: 100000000000,
used_bytes: 92000000000,
free_bytes: 8000000000,
used_pct: 92.0
},
filesystems: [
{
device: '/dev/sda1',
fstype: 'ext4',
size_bytes: 100000000000,
used_bytes: 92000000000,
avail_bytes: 8000000000,
use_pct: 92,
mountpoint: '/'
}
],
block_devices: [],
lvm: { pvs: [], vgs: [], lvs: [] },
zfs: { pools: [], datasets: [] },
feature_flags: {
has_lvm: false,
has_zfs: false,
has_lsblk: true,
has_findmnt: true
}
};
describe('Storage Details UI Component', () => {
describe('renderStorageDetailsSection', () => {
it('should return empty string when storage_details is null', () => {
const metrics = { storage_details: null };
const result = renderStorageDetailsSection(metrics, 'host-1');
expect(result).toBe('');
});
it('should return empty string when storage_details is undefined', () => {
const metrics = {};
const result = renderStorageDetailsSection(metrics, 'host-1');
expect(result).toBe('');
});
it('should render accordion header with correct title', () => {
const metrics = { storage_details: mockLinuxDebianStorageDetails };
const result = renderStorageDetailsSection(metrics, 'host-1');
expect(result).toContain('Stockage détaillé');
expect(result).toContain('fa-database');
});
it('should show status badge', () => {
const metrics = { storage_details: mockLinuxDebianStorageDetails };
const result = renderStorageDetailsSection(metrics, 'host-1');
expect(result).toContain('OK');
});
it('should show feature chips for ZFS', () => {
const metrics = { storage_details: mockTrueNASStorageDetails };
const result = renderStorageDetailsSection(metrics, 'host-1');
expect(result).toContain('ZFS');
});
it('should show usage percentage in summary', () => {
const metrics = { storage_details: mockLinuxDebianStorageDetails };
const result = renderStorageDetailsSection(metrics, 'host-1');
expect(result).toContain('25%');
});
});
describe('Filesystem Table', () => {
it('should render filesystem table with columns', () => {
const html = renderFilesystemsTable(mockLinuxDebianStorageDetails.filesystems);
expect(html).toContain('Mount');
expect(html).toContain('Device');
expect(html).toContain('FS');
expect(html).toContain('Taille');
});
it('should show mountpoint in table', () => {
const html = renderFilesystemsTable(mockLinuxDebianStorageDetails.filesystems);
expect(html).toContain('/');
});
it('should highlight high usage filesystems', () => {
const html = renderFilesystemsTable(mockHighUsageStorageDetails.filesystems);
expect(html).toContain('text-red-400');
expect(html).toContain('92%');
});
it('should filter out virtual filesystems', () => {
const filesystems = [
{ device: '/dev/sda1', mountpoint: '/', use_pct: 50 },
{ device: 'tmpfs', mountpoint: '/run', use_pct: 10 },
{ device: 'devtmpfs', mountpoint: '/dev', use_pct: 0 }
];
const html = renderFilesystemsTable(filesystems);
expect(html).not.toContain('/run');
expect(html).not.toContain('/dev');
});
});
describe('ZFS Section', () => {
it('should render ZFS pools', () => {
const html = renderZfsSection(mockTrueNASStorageDetails.zfs);
expect(html).toContain('tank');
expect(html).toContain('fa-water');
});
it('should show pool health status', () => {
const html = renderZfsSection(mockTrueNASStorageDetails.zfs);
expect(html).toContain('ONLINE');
expect(html).toContain('text-green-400');
});
it('should show pool usage percentage', () => {
const html = renderZfsSection(mockTrueNASStorageDetails.zfs);
expect(html).toContain('70%');
});
it('should render datasets list', () => {
const html = renderZfsSection(mockTrueNASStorageDetails.zfs);
expect(html).toContain('tank/apps');
});
it('should return empty string when no ZFS', () => {
const html = renderZfsSection({ pools: [], datasets: [] });
expect(html).toBe('');
});
});
describe('Inspector Mode', () => {
it('should show inspector button', () => {
const metrics = { storage_details: mockLinuxDebianStorageDetails };
const result = renderStorageDetailsSection(metrics, 'host-1');
expect(result).toContain('fa-info-circle');
expect(result).toContain('toggleStorageInspector');
});
it('should show OS type in inspector', () => {
const html = renderInspectorDrawer(mockLinuxDebianStorageDetails, 'host-1', true);
expect(html).toContain('linux-debian');
expect(html).toContain('OS détecté');
});
it('should show commands run in inspector', () => {
const html = renderInspectorDrawer(mockLinuxDebianStorageDetails, 'host-1', true);
expect(html).toContain('lsblk');
expect(html).toContain('findmnt');
expect(html).toContain('df');
});
it('should show collection timestamp', () => {
const html = renderInspectorDrawer(mockLinuxDebianStorageDetails, 'host-1', true);
expect(html).toContain('Collecté le');
});
});
describe('Warning Thresholds', () => {
it('should apply warning class for usage >= 75%', () => {
const fs = { use_pct: 80, mountpoint: '/var' };
const colorClass = getPctColor(fs.use_pct);
expect(colorClass).toBe('bg-yellow-500');
});
it('should apply critical class for usage >= 90%', () => {
const fs = { use_pct: 95, mountpoint: '/' };
const colorClass = getPctColor(fs.use_pct);
expect(colorClass).toBe('bg-red-500');
});
it('should apply normal class for usage < 75%', () => {
const fs = { use_pct: 50, mountpoint: '/home' };
const colorClass = getPctColor(fs.use_pct);
expect(colorClass).toBe('bg-green-500');
});
});
describe('Byte Formatting', () => {
it('should format bytes to GB', () => {
const result = formatBytes(107374182400); // 100 GB
expect(result).toBe('100 GB');
});
it('should format bytes to TB for large values', () => {
const result = formatBytes(1099511627776); // 1 TB
expect(result).toBe('1.0 TB');
});
it('should return empty string for zero', () => {
const result = formatBytes(0);
expect(result).toBe('');
});
it('should handle small values', () => {
const result = formatBytes(1073741824); // 1 GB
expect(result).toBe('1.0 GB');
});
});
});
// Helper functions to test (would be extracted from main.js in real implementation)
function renderStorageDetailsSection(metrics, hostId) {
const storageDetails = metrics?.storage_details;
if (!storageDetails) return '';
const status = storageDetails.status || 'unknown';
const flags = storageDetails.feature_flags || {};
const summary = storageDetails.summary || {};
const chips = [];
if (flags.has_zfs) chips.push('ZFS');
if (flags.has_lvm) chips.push('LVM');
const usedPct = summary.used_pct ? Number(summary.used_pct).toFixed(0) : null;
const statusBadge = status === 'ok' ? 'bg-green-500/20 text-green-400' :
status === 'partial' ? 'bg-yellow-500/20 text-yellow-400' : 'bg-red-500/20 text-red-400';
const statusText = status === 'ok' ? 'OK' : status === 'partial' ? 'Partiel' : 'Erreur';
return `
<div class="mt-3 border-t border-gray-700/50 pt-3">
<div class="flex items-center gap-2">
<i class="fas fa-database text-indigo-400"></i>
<span class="text-xs text-gray-400 font-medium">Stockage détaillé</span>
<span class="px-1.5 py-0.5 rounded text-[9px] ${statusBadge}">${statusText}</span>
${chips.map(c => `<span class="text-[9px]">${c}</span>`).join('')}
</div>
<span class="text-[10px] text-gray-500">${usedPct}% utilisé</span>
<button onclick="dashboard.toggleStorageInspector('${hostId}')" class="text-gray-500">
<i class="fas fa-info-circle"></i>
</button>
</div>
`;
}
function renderFilesystemsTable(filesystems) {
if (!filesystems.length) return '';
const filtered = filesystems.filter(fs => {
const mp = (fs.mountpoint || '').toLowerCase();
const dev = (fs.device || '').toLowerCase();
return !mp.startsWith('/run') && !mp.startsWith('/sys') && !mp.startsWith('/proc') &&
!dev.includes('tmpfs') && !dev.includes('devtmpfs');
});
const sanitizeDevice = (device) => {
const d = device || '';
return (typeof d === 'string' && d.startsWith('/dev/')) ? d.slice('/dev/'.length) : d;
};
const rows = filtered.map(fs => {
const pct = fs.use_pct || 0;
const device = sanitizeDevice(fs.device || '');
const pctClass = pct >= 90 ? 'text-red-400' : pct >= 75 ? 'text-yellow-400' : 'text-green-400';
return `<tr><td>${fs.mountpoint}</td><td>${device}</td><td>${fs.fstype || ''}</td><td class="${pctClass}">${pct}%</td></tr>`;
}).join('');
return `
<table class="w-full text-[11px]">
<thead>
<tr><th>Mount</th><th>Device</th><th>FS</th><th>Taille</th></tr>
</thead>
<tbody>
${rows}
</tbody>
</table>
`;
}
function renderZfsSection(zfs) {
if (!zfs.pools.length && !zfs.datasets.length) return '';
const poolCards = zfs.pools.map(pool => {
const healthColor = pool.health === 'ONLINE' ? 'text-green-400' : 'text-red-400';
return `
<div class="p-2 rounded">
<span><i class="fas fa-water"></i>${pool.name}</span>
<span class="${healthColor}">${pool.health}</span>
<span>${pool.cap_pct}%</span>
</div>
`;
}).join('');
const datasetsList = zfs.datasets.map(ds => `<div>${ds.name}</div>`).join('');
return `<div>${poolCards}${datasetsList}</div>`;
}
function renderInspectorDrawer(storageDetails, hostId, isOpen) {
if (!isOpen) return '';
const cmdList = storageDetails.commands_run.map(cmd =>
`<div>${cmd.cmd} - ${cmd.status}</div>`
).join('');
return `
<div class="inspector">
<div>OS détecté: ${storageDetails.os_type}</div>
<div>Collecté le: ${storageDetails.collected_at}</div>
<div>Commandes: ${cmdList}</div>
</div>
`;
}
function getPctColor(pct) {
if (pct === null || pct === undefined) return 'bg-gray-600';
return pct >= 90 ? 'bg-red-500' : pct >= 75 ? 'bg-yellow-500' : 'bg-green-500';
}
function formatBytes(bytes) {
if (!bytes || bytes <= 0) return '';
const gb = bytes / (1024 * 1024 * 1024);
if (gb >= 1024) return `${(gb / 1024).toFixed(1)} TB`;
if (gb >= 100) return `${gb.toFixed(0)} GB`;
return `${gb.toFixed(1)} GB`;
}