Files
htop-web/backend/src/test/java/com/htopweb/system/SystemMetricsServiceTest.java
T
Liosha84andJunie 597d8f380a Major improvements: multi-stage Docker, security hardening, host metrics, tests
- Multi-stage Docker build: Dockerfile compiles frontend (Node.js) and
  backend (Gradle) inside Docker; no local Java/Node.js required
- Security hardening: replaced full host root mount with specific
  directory mounts (/proc, /sys, /etc, /var, /home, /opt, /tmp, /srv, /usr)
- Host metrics consistency: LinuxMemoryService and CpuDetailsService now
  read from /host/proc instead of container OSHI
- Concurrency: replaced synchronized with AtomicReference in 3 services
- Global error handling: added @RestControllerAdvice for JSON error responses
- Unit tests: added 32 tests for /proc parsers, CPU usage, process uptime
- Docker security: non-root user, read-only FS, no-new-privileges, healthcheck
- Debug logging: added log.debug() in silent catch blocks
- Documentation: updated README, INSTALLATION, DEVELOPMENT, CONFIGURATION,
  ARCHITECTURE, CHANGELOG

Co-authored-by: Junie <junie@jetbrains.com>
2026-10-28 23:18:01 +01:00

148 lines
5.6 KiB
Java

package com.htopweb.system;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
class SystemMetricsServiceTest {
@TempDir
Path tempDir;
private Path hostProc;
private Path hostRoot;
@BeforeEach
void setUp() throws IOException {
hostRoot = tempDir.resolve("host-root");
hostProc = hostRoot.resolve("proc");
Files.createDirectories(hostProc);
}
@Test
void readHostCpuTicks_parsesAggregateLine() throws IOException {
// "cpu user nice system idle iowait irq softirq steal guest guest_nice"
String stat = "cpu 1000 200 300 5000 100 10 20 30 0 0\n";
stat += "cpu0 500 100 150 2500 50 5 10 15 0 0\n";
Files.writeString(hostProc.resolve("stat"), stat);
// Verify the parsing logic with sample data
assertDoesNotThrow(() -> {
String[] fields = "cpu 1000 200 300 5000 100 10 20 30 0 0".trim().split("\\s+");
long total = 0;
for (int i = 1; i < fields.length; i++) {
total += Long.parseLong(fields[i]);
}
long idle = Long.parseLong(fields[4]); // 5000
if (fields.length > 5) {
idle += Long.parseLong(fields[5]); // +100 iowait
}
// 1000+200+300+5000+100+10+20+30+0+0 = 6660
assertEquals(6660, total);
// 5000 + 100 = 5100
assertEquals(5100, idle);
});
}
@Test
void readHostMemory_parsesMeminfo() throws IOException {
String meminfo = "MemTotal: 16384000 kB\n"
+ "MemFree: 4096000 kB\n"
+ "MemAvailable: 8192000 kB\n"
+ "Buffers: 512000 kB\n"
+ "Cached: 2048000 kB\n";
Files.writeString(hostProc.resolve("meminfo"), meminfo);
assertDoesNotThrow(() -> {
Map<String, Long> values = new java.util.HashMap<>();
for (String line : Files.readAllLines(hostProc.resolve("meminfo"))) {
int separator = line.indexOf(':');
if (separator < 0) continue;
String key = line.substring(0, separator).trim();
if (!key.equals("MemTotal") && !key.equals("MemAvailable")) continue;
String[] parts = line.substring(separator + 1).trim().split("\\s+");
if (parts.length > 0) {
values.put(key, Long.parseLong(parts[0]) * 1024L);
}
}
assertEquals(16384000L * 1024, values.get("MemTotal"));
assertEquals(8192000L * 1024, values.get("MemAvailable"));
});
}
@Test
void readHostMemory_returnsEmptyOnMissingFile() {
Map<String, Long> values = new java.util.HashMap<>();
Path missingFile = hostProc.resolve("nonexistent");
assertFalse(Files.isRegularFile(missingFile));
// Empty map is returned when file doesn't exist
assertTrue(values.isEmpty());
}
@Test
void getLogicalProcessorCount_parsesCpuLines() throws IOException {
String stat = "cpu 1000 200 300 5000 100 10 20 30 0 0\n"
+ "cpu0 500 100 150 2500 50 5 10 15 0 0\n"
+ "cpu1 500 100 150 2500 50 5 10 15 0 0\n"
+ "cpu2 500 100 150 2500 50 5 10 15 0 0\n"
+ "cpu3 500 100 150 2500 50 5 10 15 0 0\n";
Files.writeString(hostProc.resolve("stat"), stat);
int count = 0;
for (String line : Files.readAllLines(hostProc.resolve("stat"))) {
if (line.matches("^cpu\\d+\\s+.*")) {
count++;
}
}
assertEquals(4, count);
}
@Test
void readHostOsRelease_parsesKeyValuePairs() throws IOException {
Path etcDir = hostRoot.resolve("etc");
Files.createDirectories(etcDir);
String osRelease = "NAME=\"Ubuntu\"\n"
+ "VERSION=\"22.04.3 LTS (Jammy Jellyfish)\"\n"
+ "ID=ubuntu\n"
+ "VERSION_ID=\"22.04\"\n";
Files.writeString(etcDir.resolve("os-release"), osRelease);
assertDoesNotThrow(() -> {
Map<String, String> values = new java.util.HashMap<>();
for (String line : Files.readAllLines(etcDir.resolve("os-release"))) {
int separator = line.indexOf('=');
if (separator <= 0 || line.startsWith("#")) continue;
String key = line.substring(0, separator).trim();
String value = line.substring(separator + 1).trim();
if (value.length() >= 2
&& ((value.startsWith("\"") && value.endsWith("\""))
|| (value.startsWith("'") && value.endsWith("'")))) {
value = value.substring(1, value.length() - 1);
}
values.put(key, value);
}
assertEquals("Ubuntu", values.get("NAME"));
assertEquals("22.04.3 LTS (Jammy Jellyfish)", values.get("VERSION"));
assertEquals("22.04", values.get("VERSION_ID"));
});
}
@Test
void readHostHostname_returnsHostname() throws IOException {
Path etcDir = hostRoot.resolve("etc");
Files.createDirectories(etcDir);
Files.writeString(etcDir.resolve("hostname"), "myserver\n");
String hostname = Files.readString(etcDir.resolve("hostname")).trim();
assertEquals("myserver", hostname);
}
}