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>
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user