test(backend): add process manager and steamcmd tests with stub scripts
- server_process_test.go: 16 tests covering splitArgs, hasArgPrefix, buildAutoArgs (config/port/profiles), buildArgs, resolveModPath (name/ ID/empty), buildModPath, WriteUserconfigFiles, Start/Stop lifecycle, percent-command substitution - steamcmd_test.go: 9 tests covering CheckWorkshopMod (directory/file/ empty), SteamCmdManager state machine, DownloadMods/UpdateGame - testdata/: stub arma3server_x64 and steamcmd scripts for safe process testing without real binaries
This commit is contained in:
@@ -0,0 +1,514 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"arma3-web-server/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSplitArgs(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
raw string
|
||||||
|
want []string
|
||||||
|
}{
|
||||||
|
{"empty", "", nil},
|
||||||
|
{"single", "-server", []string{"-server"}},
|
||||||
|
{"multiple", "-server -port=2302 -world=empty", []string{"-server", "-port=2302", "-world=empty"}},
|
||||||
|
{"extra spaces", " -server -port=2302 ", []string{"-server", "-port=2302"}},
|
||||||
|
{"tabs", "-server\t-port=2302", []string{"-server", "-port=2302"}},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := splitArgs(tt.raw)
|
||||||
|
if len(got) != len(tt.want) {
|
||||||
|
t.Fatalf("splitArgs(%q) = %v, want %v", tt.raw, got, tt.want)
|
||||||
|
}
|
||||||
|
for i := range got {
|
||||||
|
if got[i] != tt.want[i] {
|
||||||
|
t.Errorf("splitArgs(%q)[%d] = %q, want %q", tt.raw, i, got[i], tt.want[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHasArgPrefix(t *testing.T) {
|
||||||
|
args := []string{"-config=foo.cfg", "-mod=@ace", "-port=2302"}
|
||||||
|
|
||||||
|
if !hasArgPrefix(args, "-config=") {
|
||||||
|
t.Error("should find -config=")
|
||||||
|
}
|
||||||
|
if !hasArgPrefix(args, "-mod=") {
|
||||||
|
t.Error("should find -mod=")
|
||||||
|
}
|
||||||
|
if hasArgPrefix(args, "-profiles=") {
|
||||||
|
t.Error("should not find -profiles=")
|
||||||
|
}
|
||||||
|
if hasArgPrefix(nil, "-config=") {
|
||||||
|
t.Error("nil args should return false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildAutoArgs_ConfigAndPort(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
cfgDir := filepath.Join(dir, "cfg")
|
||||||
|
profilesDir := filepath.Join(dir, "profiles")
|
||||||
|
modsDir := filepath.Join(dir, "mods")
|
||||||
|
|
||||||
|
sm := NewSettingsManager(dir)
|
||||||
|
mm := NewModlistManager(filepath.Join(dir, "modlists"))
|
||||||
|
cm := NewConfigManager(cfgDir)
|
||||||
|
streamer := NewLogStreamer()
|
||||||
|
|
||||||
|
pm := NewProcessManager(dir, modsDir, cfgDir, profilesDir, sm, mm, cm, streamer)
|
||||||
|
|
||||||
|
s := &models.ServerSettings{
|
||||||
|
ActiveConfig: "server_config",
|
||||||
|
IPPort: "0.0.0.0:2302",
|
||||||
|
}
|
||||||
|
|
||||||
|
args := pm.buildAutoArgs(s)
|
||||||
|
|
||||||
|
// Should contain -config=<cfgDir>/server_config.cfg
|
||||||
|
foundConfig := false
|
||||||
|
for _, a := range args {
|
||||||
|
if strings.HasPrefix(a, "-config=") && strings.HasSuffix(a, "server_config.cfg") {
|
||||||
|
foundConfig = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundConfig {
|
||||||
|
t.Errorf("expected -config=...server_config.cfg in args, got %v", args)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should contain -port=2302
|
||||||
|
foundPort := false
|
||||||
|
for _, a := range args {
|
||||||
|
if a == "-port=2302" {
|
||||||
|
foundPort = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundPort {
|
||||||
|
t.Errorf("expected -port=2302 in args, got %v", args)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should contain -profiles=
|
||||||
|
foundProfiles := false
|
||||||
|
for _, a := range args {
|
||||||
|
if strings.HasPrefix(a, "-profiles=") {
|
||||||
|
foundProfiles = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundProfiles {
|
||||||
|
t.Errorf("expected -profiles=... in args, got %v", args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildAutoArgs_NoConfigNoPort(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
cfgDir := filepath.Join(dir, "cfg")
|
||||||
|
profilesDir := filepath.Join(dir, "profiles")
|
||||||
|
modsDir := filepath.Join(dir, "mods")
|
||||||
|
|
||||||
|
sm := NewSettingsManager(dir)
|
||||||
|
mm := NewModlistManager(filepath.Join(dir, "modlists"))
|
||||||
|
cm := NewConfigManager(cfgDir)
|
||||||
|
streamer := NewLogStreamer()
|
||||||
|
|
||||||
|
pm := NewProcessManager(dir, modsDir, cfgDir, profilesDir, sm, mm, cm, streamer)
|
||||||
|
|
||||||
|
s := &models.ServerSettings{}
|
||||||
|
|
||||||
|
args := pm.buildAutoArgs(s)
|
||||||
|
|
||||||
|
// Should NOT contain -config= or -port=
|
||||||
|
for _, a := range args {
|
||||||
|
if strings.HasPrefix(a, "-config=") {
|
||||||
|
t.Errorf("unexpected -config= in args: %v", a)
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(a, "-port=") {
|
||||||
|
t.Errorf("unexpected -port= in args: %v", a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildAutoArgs_PortAlreadyInUserArgs(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
cfgDir := filepath.Join(dir, "cfg")
|
||||||
|
profilesDir := filepath.Join(dir, "profiles")
|
||||||
|
modsDir := filepath.Join(dir, "mods")
|
||||||
|
|
||||||
|
sm := NewSettingsManager(dir)
|
||||||
|
mm := NewModlistManager(filepath.Join(dir, "modlists"))
|
||||||
|
cm := NewConfigManager(cfgDir)
|
||||||
|
streamer := NewLogStreamer()
|
||||||
|
|
||||||
|
pm := NewProcessManager(dir, modsDir, cfgDir, profilesDir, sm, mm, cm, streamer)
|
||||||
|
|
||||||
|
s := &models.ServerSettings{
|
||||||
|
IPPort: "0.0.0.0:2302",
|
||||||
|
}
|
||||||
|
|
||||||
|
args := pm.buildAutoArgs(s)
|
||||||
|
|
||||||
|
// -port= should only appear once (from auto args)
|
||||||
|
portCount := 0
|
||||||
|
for _, a := range args {
|
||||||
|
if strings.HasPrefix(a, "-port=") {
|
||||||
|
portCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if portCount != 1 {
|
||||||
|
t.Errorf("expected exactly 1 -port= arg, got %d in %v", portCount, args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildArgs_CombinesParametersAndAutoArgs(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
cfgDir := filepath.Join(dir, "cfg")
|
||||||
|
profilesDir := filepath.Join(dir, "profiles")
|
||||||
|
modsDir := filepath.Join(dir, "mods")
|
||||||
|
|
||||||
|
sm := NewSettingsManager(dir)
|
||||||
|
mm := NewModlistManager(filepath.Join(dir, "modlists"))
|
||||||
|
cm := NewConfigManager(cfgDir)
|
||||||
|
streamer := NewLogStreamer()
|
||||||
|
|
||||||
|
pm := NewProcessManager(dir, modsDir, cfgDir, profilesDir, sm, mm, cm, streamer)
|
||||||
|
|
||||||
|
s := &models.ServerSettings{
|
||||||
|
ServerParameters: "-server -world=empty -noPause",
|
||||||
|
ActiveConfig: "main",
|
||||||
|
IPPort: "0.0.0.0:2400",
|
||||||
|
}
|
||||||
|
|
||||||
|
args := pm.buildArgs(s)
|
||||||
|
|
||||||
|
// Should start with user params
|
||||||
|
if args[0] != "-server" || args[1] != "-world=empty" || args[2] != "-noPause" {
|
||||||
|
t.Errorf("user params missing from front of args: %v", args)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should contain auto-generated args
|
||||||
|
foundConfig := false
|
||||||
|
foundPort := false
|
||||||
|
for _, a := range args {
|
||||||
|
if strings.Contains(a, "main.cfg") {
|
||||||
|
foundConfig = true
|
||||||
|
}
|
||||||
|
if a == "-port=2400" {
|
||||||
|
foundPort = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundConfig {
|
||||||
|
t.Errorf("expected config arg in %v", args)
|
||||||
|
}
|
||||||
|
if !foundPort {
|
||||||
|
t.Errorf("expected port arg in %v", args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveModPath_ByModName(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
modsDir := filepath.Join(dir, "mods")
|
||||||
|
modDir := filepath.Join(modsDir, "@CBA_A3")
|
||||||
|
os.MkdirAll(modDir, 0755)
|
||||||
|
|
||||||
|
sm := NewSettingsManager(dir)
|
||||||
|
mm := NewModlistManager(filepath.Join(dir, "modlists"))
|
||||||
|
cm := NewConfigManager(filepath.Join(dir, "cfg"))
|
||||||
|
streamer := NewLogStreamer()
|
||||||
|
|
||||||
|
pm := NewProcessManager(dir, modsDir, filepath.Join(dir, "cfg"), filepath.Join(dir, "profiles"), sm, mm, cm, streamer)
|
||||||
|
|
||||||
|
// Existing mod dir → resolved path
|
||||||
|
got := pm.resolveModPath(models.ModEntry{Name: "CBA_A3"})
|
||||||
|
if got != modDir {
|
||||||
|
t.Errorf("resolveModPath(CBA_A3) = %q, want %q", got, modDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-existing mod dir → fallback to expected path
|
||||||
|
got = pm.resolveModPath(models.ModEntry{Name: "NonExistent"})
|
||||||
|
expected := filepath.Join(modsDir, "@NonExistent")
|
||||||
|
if got != expected {
|
||||||
|
t.Errorf("resolveModPath(NonExistent) = %q, want %q", got, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveModPath_ByWorkshopID(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
workshopDir := filepath.Join(dir, "steamapps", "workshop", "content", "107410", "456789")
|
||||||
|
os.MkdirAll(workshopDir, 0755)
|
||||||
|
|
||||||
|
modsDir := filepath.Join(dir, "mods")
|
||||||
|
sm := NewSettingsManager(dir)
|
||||||
|
mm := NewModlistManager(filepath.Join(dir, "modlists"))
|
||||||
|
cm := NewConfigManager(filepath.Join(dir, "cfg"))
|
||||||
|
streamer := NewLogStreamer()
|
||||||
|
|
||||||
|
pm := NewProcessManager(dir, modsDir, filepath.Join(dir, "cfg"), filepath.Join(dir, "profiles"), sm, mm, cm, streamer)
|
||||||
|
|
||||||
|
got := pm.resolveModPath(models.ModEntry{ID: "456789"})
|
||||||
|
if got != workshopDir {
|
||||||
|
t.Errorf("resolveModPath(ID=456789) = %q, want %q", got, workshopDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveModPath_EmptyEntry(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
modsDir := filepath.Join(dir, "mods")
|
||||||
|
sm := NewSettingsManager(dir)
|
||||||
|
mm := NewModlistManager(filepath.Join(dir, "modlists"))
|
||||||
|
cm := NewConfigManager(filepath.Join(dir, "cfg"))
|
||||||
|
streamer := NewLogStreamer()
|
||||||
|
|
||||||
|
pm := NewProcessManager(dir, modsDir, filepath.Join(dir, "cfg"), filepath.Join(dir, "profiles"), sm, mm, cm, streamer)
|
||||||
|
|
||||||
|
got := pm.resolveModPath(models.ModEntry{})
|
||||||
|
if got != "" {
|
||||||
|
t.Errorf("resolveModPath(empty) = %q, want empty", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildModPath_MultipleMods(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
modsDir := filepath.Join(dir, "mods")
|
||||||
|
os.MkdirAll(filepath.Join(modsDir, "@CBA_A3"), 0755)
|
||||||
|
os.MkdirAll(filepath.Join(modsDir, "@ACE3"), 0755)
|
||||||
|
|
||||||
|
sm := NewSettingsManager(dir)
|
||||||
|
mm := NewModlistManager(filepath.Join(dir, "modlists"))
|
||||||
|
cm := NewConfigManager(filepath.Join(dir, "cfg"))
|
||||||
|
streamer := NewLogStreamer()
|
||||||
|
|
||||||
|
pm := NewProcessManager(dir, modsDir, filepath.Join(dir, "cfg"), filepath.Join(dir, "profiles"), sm, mm, cm, streamer)
|
||||||
|
|
||||||
|
ml, _ := mm.Create("Test List")
|
||||||
|
mm.Update(ml.ID, "Test List", []models.ModEntry{
|
||||||
|
{Name: "CBA_A3", Enabled: true},
|
||||||
|
{Name: "ACE3", Enabled: true},
|
||||||
|
{Name: "DisabledMod", Enabled: false},
|
||||||
|
})
|
||||||
|
|
||||||
|
got := pm.buildModPath(ml.ID)
|
||||||
|
if !strings.HasPrefix(got, "-mod=") {
|
||||||
|
t.Errorf("expected -mod= prefix, got %q", got)
|
||||||
|
}
|
||||||
|
if !strings.Contains(got, "CBA_A3") || !strings.Contains(got, "ACE3") {
|
||||||
|
t.Errorf("expected CBA_A3 and ACE3 in mod path, got %q", got)
|
||||||
|
}
|
||||||
|
// Disabled mod should not appear
|
||||||
|
if strings.Contains(got, "DisabledMod") {
|
||||||
|
t.Errorf("DisabledMod should not be in mod path: %q", got)
|
||||||
|
}
|
||||||
|
// Separator should be ;
|
||||||
|
if !strings.Contains(got, ";") {
|
||||||
|
t.Errorf("expected ; separator in mod path, got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildModPath_EmptyModlist(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
modsDir := filepath.Join(dir, "mods")
|
||||||
|
sm := NewSettingsManager(dir)
|
||||||
|
mm := NewModlistManager(filepath.Join(dir, "modlists"))
|
||||||
|
cm := NewConfigManager(filepath.Join(dir, "cfg"))
|
||||||
|
streamer := NewLogStreamer()
|
||||||
|
|
||||||
|
pm := NewProcessManager(dir, modsDir, filepath.Join(dir, "cfg"), filepath.Join(dir, "profiles"), sm, mm, cm, streamer)
|
||||||
|
|
||||||
|
got := pm.buildModPath("nonexistent-id")
|
||||||
|
if got != "" {
|
||||||
|
t.Errorf("buildModPath(nonexistent) = %q, want empty", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteUserconfigFiles(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
modsDir := filepath.Join(dir, "mods")
|
||||||
|
sm := NewSettingsManager(dir)
|
||||||
|
mm := NewModlistManager(filepath.Join(dir, "modlists"))
|
||||||
|
cm := NewConfigManager(filepath.Join(dir, "cfg"))
|
||||||
|
streamer := NewLogStreamer()
|
||||||
|
|
||||||
|
pm := NewProcessManager(dir, modsDir, filepath.Join(dir, "cfg"), filepath.Join(dir, "profiles"), sm, mm, cm, streamer)
|
||||||
|
|
||||||
|
s := &models.ServerSettings{
|
||||||
|
CBASettings: "force = 1",
|
||||||
|
AILevelPresets: "preset1",
|
||||||
|
DifficultyPresets: "difficulty_normal",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := pm.WriteUserconfigFiles(s); err != nil {
|
||||||
|
t.Fatalf("WriteUserconfigFiles() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
userconfigDir := filepath.Join(dir, "userconfig")
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
filename string
|
||||||
|
content string
|
||||||
|
}{
|
||||||
|
{"cba_settings.sqf", "force = 1"},
|
||||||
|
{"CfgAILevelPresets.sqf", "preset1"},
|
||||||
|
{"CfgDifficultyPresets.sqf", "difficulty_normal"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
path := filepath.Join(userconfigDir, tt.filename)
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("ReadFile(%s) error = %v", tt.filename, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if string(data) != tt.content {
|
||||||
|
t.Errorf("%s content = %q, want %q", tt.filename, string(data), tt.content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteUserconfigFiles_EmptyContent(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
modsDir := filepath.Join(dir, "mods")
|
||||||
|
sm := NewSettingsManager(dir)
|
||||||
|
mm := NewModlistManager(filepath.Join(dir, "modlists"))
|
||||||
|
cm := NewConfigManager(filepath.Join(dir, "cfg"))
|
||||||
|
streamer := NewLogStreamer()
|
||||||
|
|
||||||
|
pm := NewProcessManager(dir, modsDir, filepath.Join(dir, "cfg"), filepath.Join(dir, "profiles"), sm, mm, cm, streamer)
|
||||||
|
|
||||||
|
s := &models.ServerSettings{}
|
||||||
|
|
||||||
|
if err := pm.WriteUserconfigFiles(s); err != nil {
|
||||||
|
t.Fatalf("WriteUserconfigFiles() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Files should still be created (even if empty)
|
||||||
|
for _, name := range []string{"cba_settings.sqf", "CfgAILevelPresets.sqf", "CfgDifficultyPresets.sqf"} {
|
||||||
|
path := filepath.Join(dir, "userconfig", name)
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
t.Errorf("expected %s to exist", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStartAndStopWithStub(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
serverfileDir := filepath.Join(dir, "server")
|
||||||
|
modsDir := filepath.Join(dir, "mods")
|
||||||
|
cfgDir := filepath.Join(dir, "cfg")
|
||||||
|
profilesDir := filepath.Join(dir, "profiles")
|
||||||
|
os.MkdirAll(serverfileDir, 0755)
|
||||||
|
|
||||||
|
// Copy stub into serverfileDir so exec finds it
|
||||||
|
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(cfgDir)
|
||||||
|
streamer := NewLogStreamer()
|
||||||
|
|
||||||
|
pm := NewProcessManager(serverfileDir, modsDir, cfgDir, profilesDir, sm, mm, cm, streamer)
|
||||||
|
|
||||||
|
s, _ := sm.Load()
|
||||||
|
s.ServerParameters = "-server -world=empty"
|
||||||
|
sm.Save(s)
|
||||||
|
|
||||||
|
if err := pm.Start(); err != nil {
|
||||||
|
t.Fatalf("Start() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !pm.IsRunning() {
|
||||||
|
t.Fatal("IsRunning() should be true after Start()")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second Start should fail (already running)
|
||||||
|
if err := pm.Start(); err == nil {
|
||||||
|
t.Error("second Start() should fail")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := pm.Stop(); err != nil {
|
||||||
|
t.Fatalf("Stop() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Give goroutine time to update state
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
if pm.IsRunning() {
|
||||||
|
t.Error("IsRunning() should be false after Stop()")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second Stop should fail
|
||||||
|
if err := pm.Stop(); err == nil {
|
||||||
|
t.Error("second Stop() should fail")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStartWithPercentCommand(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
serverfileDir := filepath.Join(dir, "server")
|
||||||
|
modsDir := filepath.Join(dir, "mods")
|
||||||
|
cfgDir := filepath.Join(dir, "cfg")
|
||||||
|
profilesDir := filepath.Join(dir, "profiles")
|
||||||
|
os.MkdirAll(serverfileDir, 0755)
|
||||||
|
|
||||||
|
// Copy stub
|
||||||
|
stubSrc, _ := os.ReadFile("testdata/arma3server_x64")
|
||||||
|
stubPath := filepath.Join(serverfileDir, "arma3server_x64")
|
||||||
|
os.WriteFile(stubPath, stubSrc, 0755)
|
||||||
|
|
||||||
|
sm := NewSettingsManager(dir)
|
||||||
|
mm := NewModlistManager(filepath.Join(dir, "modlists"))
|
||||||
|
cm := NewConfigManager(cfgDir)
|
||||||
|
streamer := NewLogStreamer()
|
||||||
|
|
||||||
|
pm := NewProcessManager(serverfileDir, modsDir, cfgDir, profilesDir, sm, mm, cm, streamer)
|
||||||
|
|
||||||
|
s, _ := sm.Load()
|
||||||
|
s.ServerParameters = "%command% -world=empty"
|
||||||
|
sm.Save(s)
|
||||||
|
|
||||||
|
if err := pm.Start(); err != nil {
|
||||||
|
t.Fatalf("Start() with %%command%% error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !pm.IsRunning() {
|
||||||
|
t.Fatal("IsRunning() should be true")
|
||||||
|
}
|
||||||
|
|
||||||
|
pm.Stop()
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStartWithEmptyCommand(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
serverfileDir := filepath.Join(dir, "server")
|
||||||
|
os.MkdirAll(serverfileDir, 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)
|
||||||
|
|
||||||
|
s, _ := sm.Load()
|
||||||
|
s.ServerParameters = ""
|
||||||
|
sm.Save(s)
|
||||||
|
|
||||||
|
// Empty params without %command% should still work (starts with just the binary)
|
||||||
|
// But if we use %command% with nothing after it, it should fail
|
||||||
|
s.ServerParameters = "%command%"
|
||||||
|
sm.Save(s)
|
||||||
|
|
||||||
|
err := pm.Start()
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Start() with percent-command-percent only (no other args) should fail")
|
||||||
|
pm.Stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCheckWorkshopMod(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
|
||||||
|
// Existing workshop mod
|
||||||
|
modDir := filepath.Join(dir, "steamapps", "workshop", "content", "107410", "123456")
|
||||||
|
os.MkdirAll(modDir, 0755)
|
||||||
|
|
||||||
|
if !CheckWorkshopMod(dir, "123456") {
|
||||||
|
t.Error("CheckWorkshopMod should return true for existing mod")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-existing mod
|
||||||
|
if CheckWorkshopMod(dir, "999999") {
|
||||||
|
t.Error("CheckWorkshopMod should return false for non-existing mod")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty mod ID
|
||||||
|
if CheckWorkshopMod(dir, "") {
|
||||||
|
t.Error("CheckWorkshopMod should return false for empty mod ID")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckWorkshopMod_IsFile(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
|
||||||
|
// Create a file instead of directory at the expected path
|
||||||
|
workshopDir := filepath.Join(dir, "steamapps", "workshop", "content", "107410")
|
||||||
|
os.MkdirAll(workshopDir, 0755)
|
||||||
|
os.WriteFile(filepath.Join(workshopDir, "111111"), []byte("not a dir"), 0644)
|
||||||
|
|
||||||
|
if CheckWorkshopMod(dir, "111111") {
|
||||||
|
t.Error("CheckWorkshopMod should return false for file (not directory)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSteamCmdManager_IsRunning(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
streamer := NewLogStreamer()
|
||||||
|
|
||||||
|
sm := NewSteamCmdManager(dir, streamer)
|
||||||
|
|
||||||
|
if sm.IsRunning() {
|
||||||
|
t.Error("IsRunning() should be false initially")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSteamCmdManager_DoubleStart(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
streamer := NewLogStreamer()
|
||||||
|
|
||||||
|
sm := NewSteamCmdManager(dir, streamer)
|
||||||
|
|
||||||
|
// First DownloadMods sets running=true. If the binary exists, the goroutine
|
||||||
|
// handles cleanup. If it doesn't, running is reset synchronously.
|
||||||
|
sm.DownloadMods([]string{"123456"})
|
||||||
|
|
||||||
|
// Second call while running should get "already running"
|
||||||
|
err2 := sm.DownloadMods([]string{"789012"})
|
||||||
|
if err2 == nil || err2.Error() != "steamcmd already running" {
|
||||||
|
t.Logf("second DownloadMods: %v (may vary based on timing)", err2)
|
||||||
|
}
|
||||||
|
|
||||||
|
waitForNotRunning(t, sm, 30*time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForNotRunning(t *testing.T, sm *SteamCmdManager, timeout time.Duration) {
|
||||||
|
t.Helper()
|
||||||
|
deadline := time.Now().Add(timeout)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
if !sm.IsRunning() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
time.Sleep(20 * time.Millisecond)
|
||||||
|
}
|
||||||
|
t.Errorf("IsRunning() still true after %v", timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSteamCmdManager_StopResetsFlag(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
streamer := NewLogStreamer()
|
||||||
|
|
||||||
|
sm := NewSteamCmdManager(dir, streamer)
|
||||||
|
|
||||||
|
// After run completes (success or failure), running flag should be reset
|
||||||
|
sm.DownloadMods([]string{"123"})
|
||||||
|
waitForNotRunning(t, sm, 30*time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSteamCmdManager_UpdateGameArgs(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
streamer := NewLogStreamer()
|
||||||
|
|
||||||
|
sm := NewSteamCmdManager(dir, streamer)
|
||||||
|
|
||||||
|
err := sm.UpdateGame("stable", "anonymous")
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("UpdateGame returned (expected): %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
waitForNotRunning(t, sm, 30*time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSteamCmdManager_UpdateGameBeta(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
streamer := NewLogStreamer()
|
||||||
|
|
||||||
|
sm := NewSteamCmdManager(dir, streamer)
|
||||||
|
|
||||||
|
// Test with a beta branch
|
||||||
|
err := sm.UpdateGame("creatordlc", "anonymous")
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("UpdateGame(creatordlc) returned (expected): %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
waitForNotRunning(t, sm, 30*time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSteamCmdManager_DownloadModEmpty(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
streamer := NewLogStreamer()
|
||||||
|
|
||||||
|
sm := NewSteamCmdManager(dir, streamer)
|
||||||
|
|
||||||
|
err := sm.DownloadMods([]string{})
|
||||||
|
if err == nil {
|
||||||
|
t.Error("DownloadMods with empty list should return error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSteamCmdManager_DownloadModSingle(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
streamer := NewLogStreamer()
|
||||||
|
|
||||||
|
sm := NewSteamCmdManager(dir, streamer)
|
||||||
|
|
||||||
|
err := sm.DownloadMod("123456")
|
||||||
|
// steamcmd may or may not exist; either way the call should complete
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("DownloadMod returned (expected): %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
waitForNotRunning(t, sm, 30*time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewSteamCmdManager(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
streamer := NewLogStreamer()
|
||||||
|
|
||||||
|
sm := NewSteamCmdManager(dir, streamer)
|
||||||
|
|
||||||
|
if sm.serverfileDir != dir {
|
||||||
|
t.Errorf("serverfileDir = %q, want %q", sm.serverfileDir, dir)
|
||||||
|
}
|
||||||
|
if sm.streamer != streamer {
|
||||||
|
t.Error("streamer should be set")
|
||||||
|
}
|
||||||
|
if sm.IsRunning() {
|
||||||
|
t.Error("new manager should not be running")
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
#!/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
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Stub steamcmd binary for testing.
|
||||||
|
# Logs args to stdout then exits immediately.
|
||||||
|
echo "[STUB] steamcmd called with: $@"
|
||||||
|
exit 0
|
||||||
Reference in New Issue
Block a user