From 04882b3c80b5079b2ee342a1fb15f2106f56e779 Mon Sep 17 00:00:00 2001 From: MrFastwind Date: Thu, 23 Jul 2026 21:07:49 +0200 Subject: [PATCH] test(backend): enhance stubs with logging, heartbeat, timed exit, and fake downloads arma3server_x64 stub: - Logs binary path and all arguments ([STUB] binary=... args=...) - Prints heartbeat every 5 seconds - Accepts -t N flag to exit after N seconds (for auto-exit tests) steamcmd stub: - Parses +force_install_dir and +workshop_download_item args - Creates fake mod directories at the expected workshop content path - Prints fake download progress and success logs New tests: - TestStart_StubLogsArgsAndPath: verifies arg logging via log streamer - TestStart_StubLogsBinaryPath: verifies binary path logging - TestStart_StubHeartbeatLogs: verifies heartbeat lines appear within timeout - TestStart_StubAutoExit: verifies -t flag causes server to auto-exit - TestDownloadMods_StubCreatesModDirs: verifies single mod dir creation - TestDownloadMods_StubCreatesMultipleModDirs: verifies multi-mod dir creation - TestDownloadMods_StubLogsAppear: verifies fake download logs on channel - TestDownloadMods_StubSteamcmdArgsLogged: verifies install_dir logging --- .../internal/services/server_process_test.go | 163 ++++++++++++++++++ backend/internal/services/steamcmd_test.go | 145 ++++++++++++++++ .../services/testdata/arma3server_x64 | 32 +++- backend/internal/services/testdata/steamcmd | 30 +++- 4 files changed, 365 insertions(+), 5 deletions(-) diff --git a/backend/internal/services/server_process_test.go b/backend/internal/services/server_process_test.go index 9007b91..c03235f 100644 --- a/backend/internal/services/server_process_test.go +++ b/backend/internal/services/server_process_test.go @@ -610,3 +610,166 @@ func TestServerParamsEnvOverridesSettings(t *testing.T) { pm.Stop() time.Sleep(100 * time.Millisecond) } + +// helper: sets up a ProcessManager with the stub and optional server params +func setupStubPM(t *testing.T, serverParams string) (*ProcessManager, *LogStreamer) { + t.Helper() + dir := t.TempDir() + serverfileDir := filepath.Join(dir, "server") + os.MkdirAll(serverfileDir, 0755) + + stubSrc, _ := os.ReadFile("testdata/arma3server_x64") + os.WriteFile(filepath.Join(serverfileDir, "arma3server_x64"), stubSrc, 0755) + + sm := NewSettingsManager(dir) + mm := NewModlistManager(filepath.Join(dir, "modlists")) + cm := NewConfigManager(filepath.Join(dir, "cfg")) + streamer := NewLogStreamer() + + pm := NewProcessManager(serverfileDir, filepath.Join(dir, "mods"), filepath.Join(dir, "cfg"), filepath.Join(dir, "profiles"), sm, mm, cm, streamer) + + if serverParams != "" { + s, _ := sm.Load() + s.ServerParameters = serverParams + sm.Save(s) + } + + return pm, streamer +} + +// readLines reads up to n lines from ch within timeout +func readLines(ch chan string, n int, timeout time.Duration) []string { + var lines []string + deadline := time.Now().Add(timeout) + for len(lines) < n { + remaining := time.Until(deadline) + if remaining <= 0 { + break + } + select { + case line, ok := <-ch: + if !ok { + return lines + } + lines = append(lines, line) + case <-time.After(remaining): + return lines + } + } + return lines +} + +func containsAny(s string, substrs ...string) bool { + for _, sub := range substrs { + if strings.Contains(s, sub) { + return true + } + } + return false +} + +func TestStart_StubLogsArgsAndPath(t *testing.T) { + pm, streamer := setupStubPM(t, "-server -port=2402") + ch := streamer.Subscribe("server", "test-args") + defer streamer.Unsubscribe("server", "test-args") + + if err := pm.Start(); err != nil { + t.Fatalf("Start() error = %v", err) + } + + lines := readLines(ch, 10, 3*time.Second) + + foundArgsLog := false + for _, line := range lines { + if strings.Contains(line, "[STUB]") && strings.Contains(line, "args=") { + foundArgsLog = true + break + } + } + if !foundArgsLog { + t.Errorf("expected [STUB] args= log line, got %v", lines) + } + + pm.Stop() + time.Sleep(100 * time.Millisecond) +} + +func TestStart_StubLogsBinaryPath(t *testing.T) { + pm, streamer := setupStubPM(t, "-server -world=empty") + ch := streamer.Subscribe("server", "test-path") + defer streamer.Unsubscribe("server", "test-path") + + if err := pm.Start(); err != nil { + t.Fatalf("Start() error = %v", err) + } + + lines := readLines(ch, 10, 3*time.Second) + + foundBinaryLog := false + for _, line := range lines { + if strings.Contains(line, "[STUB]") && strings.Contains(line, "binary=") { + foundBinaryLog = true + break + } + } + if !foundBinaryLog { + t.Errorf("expected [STUB] binary= log line, got %v", lines) + } + + pm.Stop() + time.Sleep(100 * time.Millisecond) +} + +func TestStart_StubHeartbeatLogs(t *testing.T) { + pm, streamer := setupStubPM(t, "-t 12") + ch := streamer.Subscribe("server", "test-heartbeat") + defer streamer.Unsubscribe("server", "test-heartbeat") + + if err := pm.Start(); err != nil { + t.Fatalf("Start() error = %v", err) + } + + // Collect lines for up to 10s, looking for a heartbeat + lines := readLines(ch, 50, 10*time.Second) + + foundHeartbeat := false + for _, line := range lines { + if strings.Contains(line, "[STUB]") && strings.Contains(line, "heartbeat") { + foundHeartbeat = true + break + } + } + if !foundHeartbeat { + t.Errorf("expected [STUB] heartbeat log line within 10s, got %v", lines) + } + + pm.Stop() + time.Sleep(100 * time.Millisecond) +} + +func TestStart_StubAutoExit(t *testing.T) { + pm, streamer := setupStubPM(t, "-t 2") + streamer.Subscribe("server", "test-autoexit") + defer streamer.Unsubscribe("server", "test-autoexit") + + if err := pm.Start(); err != nil { + t.Fatalf("Start() error = %v", err) + } + + if !pm.IsRunning() { + t.Fatal("IsRunning() should be true immediately after Start()") + } + + // Wait for stub to exit (2s + margin) + deadline := time.Now().Add(5 * time.Second) + for time.Now().Before(deadline) { + if !pm.IsRunning() { + break + } + time.Sleep(200 * time.Millisecond) + } + + if pm.IsRunning() { + t.Error("IsRunning() should be false after stub auto-exited (timeout 2s)") + } +} diff --git a/backend/internal/services/steamcmd_test.go b/backend/internal/services/steamcmd_test.go index 26f6bd4..474f5bc 100644 --- a/backend/internal/services/steamcmd_test.go +++ b/backend/internal/services/steamcmd_test.go @@ -3,6 +3,7 @@ package services import ( "os" "path/filepath" + "strings" "testing" "time" ) @@ -185,3 +186,147 @@ func TestSteamCmdManager_SteampathEnv(t *testing.T) { waitForNotRunning(t, sm, 30*time.Second) } + +func TestDownloadMods_StubCreatesModDirs(t *testing.T) { + dir := t.TempDir() + streamer := NewLogStreamer() + + sm := NewSteamCmdManager(dir, streamer) + + stubPath, _ := filepath.Abs("testdata/steamcmd") + t.Setenv("STEAMCMD_PATH", stubPath) + + modID := "999999" + err := sm.DownloadMod(modID) + if err != nil { + t.Fatalf("DownloadMod error = %v", err) + } + + waitForNotRunning(t, sm, 30*time.Second) + + // Stub should have created the workshop mod directory + modDir := filepath.Join(dir, "steamapps", "workshop", "content", "107410", modID) + fi, err := os.Stat(modDir) + if err != nil { + t.Fatalf("mod directory should exist after stub download: %v", err) + } + if !fi.IsDir() { + t.Errorf("mod path %q should be a directory", modDir) + } +} + +func TestDownloadMods_StubCreatesMultipleModDirs(t *testing.T) { + dir := t.TempDir() + streamer := NewLogStreamer() + + sm := NewSteamCmdManager(dir, streamer) + + stubPath, _ := filepath.Abs("testdata/steamcmd") + t.Setenv("STEAMCMD_PATH", stubPath) + + mods := []string{"111111", "222222", "333333"} + err := sm.DownloadMods(mods) + if err != nil { + t.Fatalf("DownloadMods error = %v", err) + } + + waitForNotRunning(t, sm, 30*time.Second) + + for _, modID := range mods { + modDir := filepath.Join(dir, "steamapps", "workshop", "content", "107410", modID) + if _, err := os.Stat(modDir); os.IsNotExist(err) { + t.Errorf("mod directory for %s should exist", modID) + } + } +} + +func TestDownloadMods_StubLogsAppear(t *testing.T) { + dir := t.TempDir() + streamer := NewLogStreamer() + + sm := NewSteamCmdManager(dir, streamer) + + stubPath, _ := filepath.Abs("testdata/steamcmd") + t.Setenv("STEAMCMD_PATH", stubPath) + + ch := streamer.Subscribe("steamcmd", "test-dl-logs") + defer streamer.Unsubscribe("steamcmd", "test-dl-logs") + + err := sm.DownloadMod("444444") + if err != nil { + t.Fatalf("DownloadMod error = %v", err) + } + + // Read lines with generous timeout (stub may take a moment) + lines := readSteamcmdLines(ch, 20, 5*time.Second) + + foundDownloading := false + foundSuccess := false + for _, line := range lines { + if strings.Contains(line, "Downloading") && strings.Contains(line, "444444") { + foundDownloading = true + } + if strings.Contains(line, "Success") { + foundSuccess = true + } + } + if !foundDownloading { + t.Errorf("expected 'Downloading 444444' log line, got %v", lines) + } + if !foundSuccess { + t.Errorf("expected 'Success' log line, got %v", lines) + } +} + +func TestDownloadMods_StubSteamcmdArgsLogged(t *testing.T) { + dir := t.TempDir() + streamer := NewLogStreamer() + + sm := NewSteamCmdManager(dir, streamer) + + stubPath, _ := filepath.Abs("testdata/steamcmd") + t.Setenv("STEAMCMD_PATH", stubPath) + + ch := streamer.Subscribe("steamcmd", "test-args-logs") + defer streamer.Unsubscribe("steamcmd", "test-args-logs") + + err := sm.DownloadMod("555555") + if err != nil { + t.Fatalf("DownloadMod error = %v", err) + } + + lines := readSteamcmdLines(ch, 20, 5*time.Second) + + foundArgsLog := false + for _, line := range lines { + if strings.Contains(line, "[STUB]") && strings.Contains(line, "install_dir=") { + foundArgsLog = true + break + } + } + if !foundArgsLog { + t.Errorf("expected [STUB] install_dir= log line, got %v", lines) + } +} + +// readSteamcmdLines reads lines from a steamcmd log channel +func readSteamcmdLines(ch chan string, n int, timeout time.Duration) []string { + var lines []string + deadline := time.Now().Add(timeout) + for len(lines) < n { + remaining := time.Until(deadline) + if remaining <= 0 { + break + } + select { + case line, ok := <-ch: + if !ok { + return lines + } + lines = append(lines, line) + case <-time.After(remaining): + return lines + } + } + return lines +} diff --git a/backend/internal/services/testdata/arma3server_x64 b/backend/internal/services/testdata/arma3server_x64 index 5d02f03..48876d3 100755 --- a/backend/internal/services/testdata/arma3server_x64 +++ b/backend/internal/services/testdata/arma3server_x64 @@ -1,5 +1,31 @@ #!/bin/sh # Stub arma3server binary for testing. -# Logs args to stdout then sleeps until killed (simulates a running server). -echo "[STUB] arma3server_x64 called with: $@" -sleep 3600 +# Logs binary path and all arguments, prints heartbeat every 5s. +# Use -t N to exit after N seconds (for auto-exit tests). +echo "[STUB] binary=$0 args=$*" + +DURATION=0 +while [ $# -gt 0 ]; do + case "$1" in + -t) DURATION="$2"; shift 2 ;; + *) shift ;; + esac +done + +if [ "$DURATION" -gt 0 ] 2>/dev/null; then + ELAPSED=0 + while [ "$ELAPSED" -lt "$DURATION" ]; do + REMAINING=$((DURATION - ELAPSED)) + WAIT=5 + if [ "$REMAINING" -lt 5 ]; then WAIT=$REMAINING; fi + sleep "$WAIT" + ELAPSED=$((ELAPSED + WAIT)) + echo "[STUB] heartbeat elapsed=${ELAPSED}s remaining=$((REMAINING - WAIT))s" + done + echo "[STUB] server exiting after ${DURATION}s" +else + while true; do + sleep 5 + echo "[STUB] heartbeat running..." + done +fi diff --git a/backend/internal/services/testdata/steamcmd b/backend/internal/services/testdata/steamcmd index ad64b1a..f46193f 100755 --- a/backend/internal/services/testdata/steamcmd +++ b/backend/internal/services/testdata/steamcmd @@ -1,5 +1,31 @@ #!/bin/sh # Stub steamcmd binary for testing. -# Logs args to stdout then exits immediately. -echo "[STUB] steamcmd called with: $@" +# Parses +force_install_dir and +workshop_download_item args. +# Creates fake mod directories and prints fake download logs. + +INSTALL_DIR="" +MOD_IDS="" + +while [ $# -gt 0 ]; do + case "$1" in + +force_install_dir) INSTALL_DIR="$2"; shift 2 ;; + +workshop_download_item) + APP_ID="$2"; MOD_ID="$3"; shift 3 + MOD_IDS="$MOD_IDS $MOD_ID" + ;; + *) shift ;; + esac +done + +echo "[STUB] steamcmd install_dir=$INSTALL_DIR mods=$MOD_IDS" + +for MOD_ID in $MOD_IDS; do + MOD_DIR="$INSTALL_DIR/steamapps/workshop/content/107410/$MOD_ID" + echo "[STUB] Downloading item $MOD_ID (App $APP_ID)..." + echo "[STUB] Downloading 100% [//////////]" + mkdir -p "$MOD_DIR" + echo "[STUB] Success! App $APP_ID item $MOD_ID installed to $MOD_DIR" +done + +echo "[STUB] steamcmd exiting" exit 0