package services import ( "os" "path/filepath" "strings" "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") } } func TestSteamCmdManager_SteampathEnv(t *testing.T) { dir := t.TempDir() streamer := NewLogStreamer() sm := NewSteamCmdManager(dir, streamer) // Point STEAMCMD_PATH at the stub so it doesn't need the real binary stubPath, _ := filepath.Abs("testdata/steamcmd") t.Setenv("STEAMCMD_PATH", stubPath) err := sm.DownloadMod("123456") if err != nil { t.Fatalf("DownloadMod with STEAMCMD_PATH env error = %v", err) } 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 }