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
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:
2025-12-21 12:31:08 -05:00
parent 5ff7017aaa
commit 6d8432169b
137 changed files with 30407 additions and 651 deletions
+5
View File
@@ -385,6 +385,11 @@ def api_headers():
}
@pytest.fixture
def auth_headers(api_headers):
return api_headers
@pytest.fixture
def tmp_ansible_dir(tmp_path):
"""Create temporary Ansible directory structure."""
+686
View File
@@ -0,0 +1,686 @@
"""
Tests for storage details collection and parsing.
Tests cover:
- Storage details schema validation
- Normalization of storage data from different OS types
- Parsing of lsblk, df, findmnt, zpool, zfs outputs
- Feature flags detection
- Collection metadata handling
"""
import pytest
from datetime import datetime
from typing import Dict, Any
from app.schemas.host_metrics import HostMetricsCreate, HostMetricsSummary
# =============================================================================
# Fixtures: Sample storage_details data for different OS types
# =============================================================================
@pytest.fixture
def linux_debian_storage_details() -> Dict[str, Any]:
"""Sample storage_details from a Debian Linux host with ext4 + docker overlay."""
return {
"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"},
{"cmd": "lvm", "status": "ok"}
],
"partial_failures": [],
"summary": {
"total_bytes": 500107862016,
"used_bytes": 125026965504,
"free_bytes": 349600112640,
"used_pct": 25.0
},
"block_devices": [
{
"name": "sda",
"type": "disk",
"size": 500107862016,
"model": "Samsung SSD 860",
"serial": "S3YNXXXXXXXX",
"children": [
{
"name": "sda1",
"type": "part",
"size": 536870912,
"fstype": "vfat",
"mountpoint": "/boot/efi",
"uuid": "XXXX-XXXX"
},
{
"name": "sda2",
"type": "part",
"size": 499570991104,
"fstype": "ext4",
"mountpoint": "/",
"uuid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
]
}
],
"mounts": [
{
"target": "/",
"source": "/dev/sda2",
"fstype": "ext4",
"options": "rw,relatime"
},
{
"target": "/boot/efi",
"source": "/dev/sda1",
"fstype": "vfat",
"options": "rw,relatime"
}
],
"filesystems": [
{
"device": "/dev/sda2",
"fstype": "ext4",
"size_bytes": 499570991104,
"used_bytes": 125026965504,
"avail_bytes": 349600112640,
"use_pct": 26,
"mountpoint": "/"
},
{
"device": "/dev/sda1",
"fstype": "vfat",
"size_bytes": 536870912,
"used_bytes": 5242880,
"avail_bytes": 531628032,
"use_pct": 1,
"mountpoint": "/boot/efi"
}
],
"lvm": {
"pvs": [],
"vgs": [],
"lvs": []
},
"zfs": {
"pools": [],
"datasets": []
},
"feature_flags": {
"has_lvm": False,
"has_zfs": False,
"has_lsblk": True,
"has_findmnt": True
}
}
@pytest.fixture
def linux_alpine_storage_details() -> Dict[str, Any]:
"""Sample storage_details from an Alpine Linux host (minimal busybox)."""
return {
"os_type": "linux-alpine",
"status": "partial",
"collected_at": "2025-12-19T10:00:00Z",
"commands_run": [
{"cmd": "lsblk", "status": "ok"},
{"cmd": "df", "status": "ok"}
],
"partial_failures": ["findmnt parse failed"],
"summary": {
"total_bytes": 10737418240,
"used_bytes": 2147483648,
"free_bytes": 8589934592,
"used_pct": 20.0
},
"block_devices": [
{
"name": "vda",
"type": "disk",
"size": 10737418240,
"children": [
{
"name": "vda1",
"type": "part",
"size": 10736369664,
"fstype": "ext4",
"mountpoint": "/"
}
]
}
],
"mounts": [],
"filesystems": [
{
"device": "/dev/vda1",
"fstype": "ext4",
"size_bytes": 10736369664,
"used_bytes": 2147483648,
"avail_bytes": 8588886016,
"use_pct": 20,
"mountpoint": "/"
}
],
"lvm": {"pvs": [], "vgs": [], "lvs": []},
"zfs": {"pools": [], "datasets": []},
"feature_flags": {
"has_lvm": False,
"has_zfs": False,
"has_lsblk": True,
"has_findmnt": False
}
}
@pytest.fixture
def truenas_core_storage_details() -> Dict[str, Any]:
"""Sample storage_details from TrueNAS CORE (FreeBSD + ZFS)."""
return {
"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
},
"block_devices": [],
"mounts": [],
"filesystems": [
{
"device": "tank",
"fstype": "zfs",
"size_bytes": 4000787030016,
"used_bytes": 2800550921011,
"avail_bytes": 1200236109005,
"use_pct": 70,
"mountpoint": "/mnt/tank"
}
],
"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",
"used_bytes": 2800550921011,
"avail_bytes": 1200236109005,
"refer_bytes": 98304,
"mountpoint": "/mnt/tank",
"type": "filesystem"
},
{
"name": "tank/apps",
"used_bytes": 107374182400,
"avail_bytes": 1200236109005,
"refer_bytes": 107374182400,
"mountpoint": "/mnt/tank/apps",
"type": "filesystem"
},
{
"name": "tank/media",
"used_bytes": 2693176738611,
"avail_bytes": 1200236109005,
"refer_bytes": 2693176738611,
"mountpoint": "/mnt/tank/media",
"type": "filesystem"
}
]
},
"feature_flags": {
"has_lvm": False,
"has_zfs": True,
"has_lsblk": False,
"has_findmnt": False
}
}
@pytest.fixture
def truenas_scale_storage_details() -> Dict[str, Any]:
"""Sample storage_details from TrueNAS SCALE (Linux + ZFS)."""
return {
"os_type": "truenas-scale",
"status": "ok",
"collected_at": "2025-12-19T10:00:00Z",
"commands_run": [
{"cmd": "lsblk", "status": "ok"},
{"cmd": "findmnt", "status": "ok"},
{"cmd": "df", "status": "ok"},
{"cmd": "zpool list", "status": "ok"},
{"cmd": "zfs list", "status": "ok"}
],
"partial_failures": [],
"summary": {
"total_bytes": 8001574060032,
"used_bytes": 4800944436019,
"free_bytes": 3200629624013,
"used_pct": 60.0
},
"block_devices": [
{
"name": "sda",
"type": "disk",
"size": 4000787030016,
"model": "WDC WD40EFAX",
"rota": True
},
{
"name": "sdb",
"type": "disk",
"size": 4000787030016,
"model": "WDC WD40EFAX",
"rota": True
}
],
"mounts": [],
"filesystems": [
{
"device": "boot-pool",
"fstype": "zfs",
"size_bytes": 64424509440,
"used_bytes": 3221225472,
"avail_bytes": 61203283968,
"use_pct": 5,
"mountpoint": "/boot"
}
],
"lvm": {"pvs": [], "vgs": [], "lvs": []},
"zfs": {
"pools": [
{
"name": "boot-pool",
"size_bytes": 64424509440,
"alloc_bytes": 3221225472,
"free_bytes": 61203283968,
"cap_pct": 5,
"health": "ONLINE"
},
{
"name": "data",
"size_bytes": 8001574060032,
"alloc_bytes": 4800944436019,
"free_bytes": 3200629624013,
"cap_pct": 60,
"health": "ONLINE"
}
],
"datasets": [
{
"name": "data",
"used_bytes": 4800944436019,
"avail_bytes": 3200629624013,
"refer_bytes": 98304,
"mountpoint": "/mnt/data",
"type": "filesystem"
}
]
},
"feature_flags": {
"has_lvm": False,
"has_zfs": True,
"has_lsblk": True,
"has_findmnt": True
}
}
@pytest.fixture
def linux_lvm_storage_details() -> Dict[str, Any]:
"""Sample storage_details from a Linux host with LVM."""
return {
"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"},
{"cmd": "lvm", "status": "ok"}
],
"partial_failures": [],
"summary": {
"total_bytes": 1000204886016,
"used_bytes": 400081954406,
"free_bytes": 549062746931,
"used_pct": 40.0
},
"block_devices": [
{
"name": "sda",
"type": "disk",
"size": 1000204886016,
"children": [
{
"name": "sda1",
"type": "part",
"size": 536870912,
"fstype": "vfat",
"mountpoint": "/boot/efi"
},
{
"name": "sda2",
"type": "part",
"size": 999667015104,
"fstype": "LVM2_member"
}
]
}
],
"mounts": [],
"filesystems": [
{
"device": "/dev/mapper/vg0-root",
"fstype": "ext4",
"size_bytes": 107374182400,
"used_bytes": 32212254720,
"avail_bytes": 69793193984,
"use_pct": 30,
"mountpoint": "/"
},
{
"device": "/dev/mapper/vg0-home",
"fstype": "ext4",
"size_bytes": 429496729600,
"used_bytes": 214748364800,
"avail_bytes": 192673095680,
"use_pct": 50,
"mountpoint": "/home"
},
{
"device": "/dev/mapper/vg0-var",
"fstype": "ext4",
"size_bytes": 214748364800,
"used_bytes": 107374182400,
"avail_bytes": 96636764160,
"use_pct": 50,
"mountpoint": "/var"
}
],
"lvm": {
"pvs": [
{
"pv_name": "/dev/sda2",
"vg_name": "vg0",
"pv_size": "930.00g",
"pv_free": "178.00g"
}
],
"vgs": [
{
"vg_name": "vg0",
"vg_size": "930.00g",
"vg_free": "178.00g"
}
],
"lvs": [
{
"lv_name": "root",
"vg_name": "vg0",
"lv_size": "100.00g"
},
{
"lv_name": "home",
"vg_name": "vg0",
"lv_size": "400.00g"
},
{
"lv_name": "var",
"vg_name": "vg0",
"lv_size": "200.00g"
}
]
},
"zfs": {"pools": [], "datasets": []},
"feature_flags": {
"has_lvm": True,
"has_zfs": False,
"has_lsblk": True,
"has_findmnt": True
}
}
# =============================================================================
# Tests
# =============================================================================
class TestStorageDetailsSchema:
"""Tests for storage_details schema validation."""
def test_host_metrics_create_with_storage_details(self, linux_debian_storage_details):
"""Test that HostMetricsCreate accepts storage_details."""
metrics = HostMetricsCreate(
host_id="test-host",
metric_type="storage_details",
storage_details=linux_debian_storage_details
)
assert metrics.host_id == "test-host"
assert metrics.metric_type == "storage_details"
assert metrics.storage_details is not None
assert metrics.storage_details["os_type"] == "linux-debian"
assert metrics.storage_details["status"] == "ok"
def test_host_metrics_summary_includes_storage_details(self, truenas_core_storage_details):
"""Test that HostMetricsSummary can include storage_details."""
summary = HostMetricsSummary(
host_id="truenas-host",
storage_details=truenas_core_storage_details,
collection_status="success"
)
assert summary.storage_details is not None
assert summary.storage_details["feature_flags"]["has_zfs"] is True
class TestStorageDetailsOsTypes:
"""Tests for different OS type storage details."""
def test_linux_debian_has_lsblk_and_findmnt(self, linux_debian_storage_details):
"""Test that Debian Linux has lsblk and findmnt data."""
flags = linux_debian_storage_details["feature_flags"]
assert flags["has_lsblk"] is True
assert flags["has_findmnt"] is True
assert flags["has_lvm"] is False
assert flags["has_zfs"] is False
assert len(linux_debian_storage_details["block_devices"]) > 0
assert len(linux_debian_storage_details["mounts"]) > 0
def test_linux_alpine_partial_status(self, linux_alpine_storage_details):
"""Test that Alpine Linux with partial failures has correct status."""
assert linux_alpine_storage_details["status"] == "partial"
assert "findmnt parse failed" in linux_alpine_storage_details["partial_failures"]
assert linux_alpine_storage_details["feature_flags"]["has_findmnt"] is False
def test_truenas_core_has_zfs_no_lsblk(self, truenas_core_storage_details):
"""Test that TrueNAS CORE has ZFS but no lsblk."""
flags = truenas_core_storage_details["feature_flags"]
assert flags["has_zfs"] is True
assert flags["has_lsblk"] is False
assert flags["has_findmnt"] is False
zfs = truenas_core_storage_details["zfs"]
assert len(zfs["pools"]) == 1
assert zfs["pools"][0]["name"] == "tank"
assert zfs["pools"][0]["health"] == "ONLINE"
assert len(zfs["datasets"]) == 3
def test_truenas_scale_has_both_zfs_and_lsblk(self, truenas_scale_storage_details):
"""Test that TrueNAS SCALE has both ZFS and lsblk."""
flags = truenas_scale_storage_details["feature_flags"]
assert flags["has_zfs"] is True
assert flags["has_lsblk"] is True
assert flags["has_findmnt"] is True
assert len(truenas_scale_storage_details["block_devices"]) == 2
assert len(truenas_scale_storage_details["zfs"]["pools"]) == 2
def test_linux_lvm_has_volume_groups(self, linux_lvm_storage_details):
"""Test that Linux with LVM has volume groups."""
flags = linux_lvm_storage_details["feature_flags"]
assert flags["has_lvm"] is True
assert flags["has_zfs"] is False
lvm = linux_lvm_storage_details["lvm"]
assert len(lvm["pvs"]) == 1
assert len(lvm["vgs"]) == 1
assert len(lvm["lvs"]) == 3
assert lvm["vgs"][0]["vg_name"] == "vg0"
class TestStorageDetailsSummary:
"""Tests for storage summary calculations."""
def test_summary_total_bytes(self, linux_debian_storage_details):
"""Test that summary contains correct total bytes."""
summary = linux_debian_storage_details["summary"]
assert summary["total_bytes"] == 500107862016
assert summary["used_bytes"] == 125026965504
assert summary["free_bytes"] == 349600112640
assert summary["used_pct"] == 25.0
def test_summary_used_percentage_calculation(self, truenas_core_storage_details):
"""Test that used percentage is calculated correctly."""
summary = truenas_core_storage_details["summary"]
total = summary["total_bytes"]
used = summary["used_bytes"]
# Check that used_pct is approximately correct
expected_pct = (used / total) * 100
assert abs(summary["used_pct"] - expected_pct) < 1.0
class TestStorageDetailsFilesystems:
"""Tests for filesystem data parsing."""
def test_filesystem_has_required_fields(self, linux_debian_storage_details):
"""Test that filesystems have required fields."""
fs = linux_debian_storage_details["filesystems"][0]
required_fields = ["device", "fstype", "size_bytes", "used_bytes", "avail_bytes", "use_pct", "mountpoint"]
for field in required_fields:
assert field in fs, f"Missing field: {field}"
def test_filesystem_sizes_in_bytes(self, linux_debian_storage_details):
"""Test that filesystem sizes are in bytes."""
fs = linux_debian_storage_details["filesystems"][0]
# Size should be larger than 1 GB (in bytes)
assert fs["size_bytes"] > 1024 * 1024 * 1024
# Used + available should approximately equal total
total = fs["size_bytes"]
used = fs["used_bytes"]
avail = fs["avail_bytes"]
# Allow for some filesystem overhead
assert abs((used + avail) - total) < total * 0.1
class TestStorageDetailsZfs:
"""Tests for ZFS data parsing."""
def test_zfs_pool_has_required_fields(self, truenas_core_storage_details):
"""Test that ZFS pools have required fields."""
pool = truenas_core_storage_details["zfs"]["pools"][0]
required_fields = ["name", "size_bytes", "alloc_bytes", "free_bytes", "cap_pct", "health"]
for field in required_fields:
assert field in pool, f"Missing field: {field}"
def test_zfs_dataset_has_required_fields(self, truenas_core_storage_details):
"""Test that ZFS datasets have required fields."""
ds = truenas_core_storage_details["zfs"]["datasets"][0]
required_fields = ["name", "used_bytes", "avail_bytes", "refer_bytes", "mountpoint", "type"]
for field in required_fields:
assert field in ds, f"Missing field: {field}"
def test_zfs_pool_health_status(self, truenas_core_storage_details):
"""Test ZFS pool health status."""
pool = truenas_core_storage_details["zfs"]["pools"][0]
assert pool["health"] in ["ONLINE", "DEGRADED", "FAULTED", "OFFLINE", "UNAVAIL", "REMOVED"]
class TestStorageDetailsCommands:
"""Tests for command execution metadata."""
def test_commands_run_list(self, linux_debian_storage_details):
"""Test that commands_run contains expected commands."""
commands = linux_debian_storage_details["commands_run"]
cmd_names = [c["cmd"] for c in commands]
assert "lsblk" in cmd_names
assert "df" in cmd_names
def test_command_status_is_ok(self, truenas_core_storage_details):
"""Test that successful commands have status 'ok'."""
for cmd in truenas_core_storage_details["commands_run"]:
assert cmd["status"] == "ok"
def test_partial_failures_recorded(self, linux_alpine_storage_details):
"""Test that partial failures are recorded."""
assert len(linux_alpine_storage_details["partial_failures"]) > 0
assert linux_alpine_storage_details["status"] == "partial"
class TestStorageDetailsWarningThresholds:
"""Tests for warning threshold detection."""
def test_high_usage_filesystem_detection(self):
"""Test detection of filesystems with high usage."""
storage_details = {
"filesystems": [
{"mountpoint": "/", "use_pct": 50},
{"mountpoint": "/var", "use_pct": 85}, # Warning
{"mountpoint": "/home", "use_pct": 95}, # Critical
]
}
warning_mounts = [
fs["mountpoint"]
for fs in storage_details["filesystems"]
if fs["use_pct"] >= 85
]
assert "/var" in warning_mounts
assert "/home" in warning_mounts
assert "/" not in warning_mounts
def test_critical_usage_detection(self):
"""Test detection of critical usage (>95%)."""
storage_details = {
"filesystems": [
{"mountpoint": "/", "use_pct": 96},
]
}
critical_mounts = [
fs["mountpoint"]
for fs in storage_details["filesystems"]
if fs["use_pct"] >= 95
]
assert "/" in critical_mounts
@@ -0,0 +1,37 @@
import pytest
from datetime import datetime, timedelta, timezone
from unittest.mock import AsyncMock, patch
from app.models.terminal_session import TerminalSession, SESSION_STATUS_ACTIVE
class TestTerminalConnectPage:
@pytest.mark.asyncio
async def test_connect_page_renders_html(self, client, db_session):
now = datetime.now(timezone.utc)
sess = TerminalSession(
id="s" * 64,
host_id="host-1",
host_name="test-host",
host_ip="127.0.0.1",
user_id="user-1",
username="testuser",
token_hash="x" * 64,
ttyd_port=7680,
ttyd_pid=123,
mode="embedded",
status=SESSION_STATUS_ACTIVE,
created_at=now,
last_seen_at=now,
expires_at=now + timedelta(minutes=10),
)
db_session.add(sess)
await db_session.commit()
with patch("app.services.terminal_service.terminal_service.verify_token", return_value=True), \
patch("app.services.terminal_service.terminal_service.is_session_process_alive", new=AsyncMock(return_value=True)):
resp = await client.get(f"/api/terminal/connect/{sess.id}?token=dummy")
assert resp.status_code == 200
assert "<html" in resp.text.lower()
assert "terminal" in resp.text.lower()
+20
View File
@@ -0,0 +1,20 @@
import pytest
from unittest.mock import patch
class TestTerminalStatus:
@pytest.mark.asyncio
async def test_terminal_status_available_true(self, client, auth_headers):
with patch("app.services.terminal_service.terminal_service.check_ttyd_available", return_value=True):
resp = await client.get("/api/terminal/status", headers=auth_headers)
assert resp.status_code == 200
data = resp.json()
assert data["available"] is True
@pytest.mark.asyncio
async def test_terminal_status_available_false(self, client, auth_headers):
with patch("app.services.terminal_service.terminal_service.check_ttyd_available", return_value=False):
resp = await client.get("/api/terminal/status", headers=auth_headers)
assert resp.status_code == 200
data = resp.json()
assert data["available"] is False
+429
View File
@@ -0,0 +1,429 @@
/**
* 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`;
}