fix: code quality, memory safety, and install improvements
Critical fixes: - Fix Dockerfile: reorder stages so frontend assets embed into Go binary - Fix Go version 1.25 (nonexistent) to 1.24 across Dockerfile, go.mod, CI - Add graceful game server shutdown on SIGTERM/SIGINT - Order startup tasks: updates complete before auto-start - Fix TOCTOU race in UpdateSettings with atomic Update() method Security: - Add optional AUTH_TOKEN bearer auth middleware on API/WS routes - Fix path traversal in DeleteMod using filepath.Rel instead of HasPrefix - Add input validation for IPPort, ServerParameters, ScheduledUpdate Memory safety: - Cap RPT buffer allocation to 64KB to prevent OOM on large logs - Cap GetLog file read to 10MB - Fix context cancel leak in SteamCmdManager.run() - Remove data-raced cancel field in steamcmd.go - Atomic file writes (write-temp-then-rename) across all managers Reliability: - Log save errors in ProcessManager.Stop() - Atomic file writes prevent corruption on crash Tests: - Add mod_manager_test.go (12 tests: ListWorkshopMods, ListLocalMods, BuildUsageMap, RemoveMod, dirSize) - Add scheduler_test.go (6 tests: Start/Stop, Refresh with empty, invalid, valid, and replaced cron expressions) - Add TestRestart to server_process_test.go CI/Docs: - Add -race flag to go test in CI and Makefile - Add npm lint step to CI - Add Go/npm module caching to CI - Update README: prerequisites, AUTH_TOKEN/GIN_MODE/SERVERS_DIR docs, fix manual quickstart to use make build
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"arma3-web-server/internal/models"
|
||||
)
|
||||
|
||||
func TestNewScheduler(t *testing.T) {
|
||||
dataDir := t.TempDir()
|
||||
settings := NewSettingsManager(dataDir)
|
||||
modlists := NewModlistManager(dataDir)
|
||||
streamer := NewLogStreamer()
|
||||
steamcmd := NewSteamCmdManager(t.TempDir(), streamer)
|
||||
|
||||
s := NewScheduler(settings, modlists, steamcmd)
|
||||
if s == nil {
|
||||
t.Fatal("expected non-nil scheduler")
|
||||
}
|
||||
if s.cron == nil {
|
||||
t.Fatal("expected non-nil cron")
|
||||
}
|
||||
}
|
||||
|
||||
func TestScheduler_StartStop(t *testing.T) {
|
||||
dataDir := t.TempDir()
|
||||
settings := NewSettingsManager(dataDir)
|
||||
modlists := NewModlistManager(dataDir)
|
||||
streamer := NewLogStreamer()
|
||||
steamcmd := NewSteamCmdManager(t.TempDir(), streamer)
|
||||
|
||||
s := NewScheduler(settings, modlists, steamcmd)
|
||||
s.Start()
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
s.Stop()
|
||||
}
|
||||
|
||||
func TestScheduler_Refresh_Empty(t *testing.T) {
|
||||
dataDir := t.TempDir()
|
||||
settings := NewSettingsManager(dataDir)
|
||||
modlists := NewModlistManager(dataDir)
|
||||
streamer := NewLogStreamer()
|
||||
steamcmd := NewSteamCmdManager(t.TempDir(), streamer)
|
||||
|
||||
s := NewScheduler(settings, modlists, steamcmd)
|
||||
s.Start()
|
||||
defer s.Stop()
|
||||
|
||||
s.Refresh()
|
||||
if s.entryID != 0 {
|
||||
t.Errorf("expected entryID 0 with empty settings, got %d", s.entryID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScheduler_Refresh_InvalidCron(t *testing.T) {
|
||||
dataDir := t.TempDir()
|
||||
settings := NewSettingsManager(dataDir)
|
||||
modlists := NewModlistManager(dataDir)
|
||||
streamer := NewLogStreamer()
|
||||
steamcmd := NewSteamCmdManager(t.TempDir(), streamer)
|
||||
|
||||
_, err := settings.Update(func(s *models.ServerSettings) {
|
||||
s.ScheduledUpdate = "not-a-cron"
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
sched := NewScheduler(settings, modlists, steamcmd)
|
||||
sched.Start()
|
||||
defer sched.Stop()
|
||||
|
||||
sched.Refresh()
|
||||
if sched.entryID != 0 {
|
||||
t.Errorf("expected entryID 0 with invalid cron, got %d", sched.entryID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScheduler_Refresh_ValidCron(t *testing.T) {
|
||||
dataDir := t.TempDir()
|
||||
settings := NewSettingsManager(dataDir)
|
||||
modlists := NewModlistManager(dataDir)
|
||||
streamer := NewLogStreamer()
|
||||
steamcmd := NewSteamCmdManager(t.TempDir(), streamer)
|
||||
|
||||
_, err := settings.Update(func(s *models.ServerSettings) {
|
||||
s.ScheduledUpdate = "0 4 * * *"
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
sched := NewScheduler(settings, modlists, steamcmd)
|
||||
sched.Start()
|
||||
defer sched.Stop()
|
||||
|
||||
sched.Refresh()
|
||||
if sched.entryID == 0 {
|
||||
t.Errorf("expected non-zero entryID with valid cron")
|
||||
}
|
||||
}
|
||||
|
||||
func TestScheduler_Refresh_Replace(t *testing.T) {
|
||||
dataDir := t.TempDir()
|
||||
settings := NewSettingsManager(dataDir)
|
||||
modlists := NewModlistManager(dataDir)
|
||||
streamer := NewLogStreamer()
|
||||
steamcmd := NewSteamCmdManager(t.TempDir(), streamer)
|
||||
|
||||
sched := NewScheduler(settings, modlists, steamcmd)
|
||||
sched.Start()
|
||||
defer sched.Stop()
|
||||
|
||||
_, err := settings.Update(func(s *models.ServerSettings) {
|
||||
s.ScheduledUpdate = "0 4 * * *"
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sched.Refresh()
|
||||
id1 := sched.entryID
|
||||
|
||||
_, err = settings.Update(func(s *models.ServerSettings) {
|
||||
s.ScheduledUpdate = "0 5 * * *"
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sched.Refresh()
|
||||
id2 := sched.entryID
|
||||
|
||||
if id1 == id2 {
|
||||
t.Errorf("expected different entryIDs after refresh, both got %d", id1)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user