package main import ( "encoding/json" "fmt" "net/http" "net/http/httptest" "os" "path/filepath" "testing" "time" ) func TestCmdList_NoArgs(t *testing.T) { orig := osExit var exitCode int osExit = func(code int) { exitCode = code } defer func() { osExit = orig }() done := make(chan struct{}) go func() { defer close(done) cmdList([]string{}) }() <-done if exitCode != 1 { t.Errorf("expected exit code 1, got %d", exitCode) } } func TestCmdList_ServerError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) })) defer server.Close() origBase := iaBaseURL iaBaseURL = server.URL defer func() { iaBaseURL = origBase }() orig := osExit var exitCode int osExit = func(code int) { exitCode = code } defer func() { osExit = orig }() done := make(chan struct{}) go func() { defer close(done) cmdList([]string{"testitem"}) }() <-done if exitCode != 1 { t.Errorf("expected exit code 1 for server error, got %d", exitCode) } } func TestCmdSearch_FilterFlag(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { q := r.URL.Query().Get("q") if q == "" { t.Error("empty query") } resp := SearchResult{Response: struct { NumFound int `json:"numFound"` Docs []SearchDoc `json:"docs"` }{ NumFound: 0, Docs: []SearchDoc{}, }} w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(resp); err != nil { return } })) defer server.Close() origSearch := iaSearchURL iaSearchURL = server.URL + "/advancedsearch.php" defer func() { iaSearchURL = origSearch }() orig := osExit var exitCode int osExit = func(code int) { exitCode = code } defer func() { osExit = orig }() done := make(chan struct{}) go func() { defer close(done) cmdSearch([]string{"-query", "drums", "-filter", "vocal", "-limit", "5"}) }() <-done if exitCode != 0 && exitCode != 1 { t.Errorf("expected exit 0 or 1, got %d", exitCode) } } func TestCmdSearch_ZeroResults(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { resp := SearchResult{Response: struct { NumFound int `json:"numFound"` Docs []SearchDoc `json:"docs"` }{ NumFound: 0, Docs: []SearchDoc{}, }} w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(resp); err != nil { return } })) defer server.Close() origSearch := iaSearchURL iaSearchURL = server.URL + "/advancedsearch.php" defer func() { iaSearchURL = origSearch }() orig := osExit var exitCode int osExit = func(code int) { exitCode = code } defer func() { osExit = orig }() done := make(chan struct{}) go func() { defer close(done) cmdSearch([]string{"-query", "nonexistent XYZ query 12345", "-limit", "5"}) }() <-done if exitCode != 0 { t.Errorf("expected exit 0 for zero results, got %d", exitCode) } } func TestCmdSearch_MinDownloadsFilter(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { resp := SearchResult{ Response: struct { NumFound int `json:"numFound"` Docs []SearchDoc `json:"docs"` }{ NumFound: 2, Docs: []SearchDoc{ {Identifier: "low-dl", Title: "Low DL", Downloads: 5}, {Identifier: "high-dl", Title: "High DL", Downloads: 1000}, }, }, } w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(resp); err != nil { return } })) defer server.Close() origSearch := iaSearchURL iaSearchURL = server.URL + "/advancedsearch.php" defer func() { iaSearchURL = origSearch }() orig := osExit var exitCode int osExit = func(code int) { exitCode = code } defer func() { osExit = orig }() done := make(chan struct{}) go func() { defer close(done) cmdSearch([]string{"-min-downloads", "100"}) }() <-done if exitCode != 0 { t.Errorf("expected exit 0, got %d", exitCode) } } func TestGetItemFiles_MalformedJSON(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") if _, err := w.Write([]byte("not valid json{")); err != nil { return } })) defer server.Close() origBase := iaBaseURL iaBaseURL = server.URL defer func() { iaBaseURL = origBase }() orig := osExit var exitCode int osExit = func(code int) { exitCode = code } defer func() { osExit = orig }() done := make(chan struct{}) go func() { defer close(done) cmdList([]string{"badjson"}) }() <-done if exitCode != 1 { t.Errorf("expected exit 1 for malformed JSON, got %d", exitCode) } } func TestGetItemFiles_EmptyIdentifier(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") if _, err := w.Write([]byte(`{"files":[]}`)); err != nil { return } })) defer server.Close() origBase := iaBaseURL iaBaseURL = server.URL defer func() { iaBaseURL = origBase }() orig := osExit var exitCode int osExit = func(code int) { exitCode = code } defer func() { osExit = orig }() done := make(chan struct{}) go func() { defer close(done) cmdList([]string{""}) }() <-done if exitCode != 0 { t.Errorf("expected exit 0 for empty identifier (empty item), got %d", exitCode) } } func TestSanitiseISOName_EvenMoreCases(t *testing.T) { cases := []struct { input string expected string }{ {"AKAI CD VOL.1", "AKAI_CD_VOL"}, {"S900 COMPACT DISK", "S900_COMPACT_DISK"}, {"PROFESSIONAL SAMPLES VOL_1", "PROFESSIONAL_SAMPLES_VOL_1"}, {"disk image (iso)", "disk_image_iso"}, {"folder/", "folder"}, {"...dots...", "dots"}, {"all!@#$%special", "all_special"}, {"tab\there", "tab_here"}, } for _, c := range cases { got := sanitiseISOName(c.input) if got != c.expected { t.Errorf("sanitiseISOName(%q) = %q, want %q", c.input, got, c.expected) } } } func TestCmdExtract_WithISOs(t *testing.T) { orig := extractISO extractISO = func(isoPath, outDir string) (string, error) { return "Total WAV files: 10 (3s)", nil } defer func() { extractISO = orig }() tmp := t.TempDir() isoDir := filepath.Join(tmp, "isos") if err := os.MkdirAll(isoDir, 0755); err != nil { t.Fatal(err) } fd, _ := os.Create(filepath.Join(isoDir, "test.iso")) defer func() { _ = fd.Close() }() done := make(chan struct{}) var exitCode int go func() { defer func() { if e := recover(); e != nil { if fe, ok := e.(fatalErr); ok { exitCode = fe.code } } close(done) }() cmdExtract([]string{"-dir", isoDir, "-out", filepath.Join(tmp, "wavs"), "-jobs", "1"}) }() select { case <-done: case <-time.After(3 * time.Second): t.Fatal("test timed out — possible goroutine leak") } if exitCode != 0 { t.Errorf("expected exit 0, got %d", exitCode) } } func TestCmdExtract_NoISOs(t *testing.T) { orig := extractISO extractISO = func(isoPath, outDir string) (string, error) { return "should not be called", nil } defer func() { extractISO = orig }() tmp := t.TempDir() isoDir := filepath.Join(tmp, "empty_isos") if err := os.MkdirAll(isoDir, 0755); err != nil { t.Fatal(err) } done := make(chan struct{}) var exitCode int go func() { defer func() { if e := recover(); e != nil { if fe, ok := e.(fatalErr); ok { exitCode = fe.code } } close(done) }() cmdExtract([]string{"-dir", isoDir, "-out", filepath.Join(tmp, "wavs")}) }() select { case <-done: case <-time.After(3 * time.Second): t.Fatal("test timed out") } if exitCode != 0 { t.Errorf("expected exit 0 for no ISOs (print and return), got %d", exitCode) } } func TestCmdExtract_BadOutDir(t *testing.T) { orig := extractISO extractISO = func(isoPath, outDir string) (string, error) { return "stubbed", nil } defer func() { extractISO = orig }() tmp := t.TempDir() isoDir := filepath.Join(tmp, "isos") if err := os.MkdirAll(isoDir, 0755); err != nil { t.Fatal(err) } fd, _ := os.Create(filepath.Join(isoDir, "test.iso")) defer func() { _ = fd.Close() }() outDir := filepath.Join(tmp, "nonexistent", "wavs") done := make(chan struct{}) var exitCode int go func() { defer func() { if e := recover(); e != nil { if fe, ok := e.(fatalErr); ok { exitCode = fe.code } } close(done) }() cmdExtract([]string{"-dir", isoDir, "-out", outDir}) }() select { case <-done: case <-time.After(3 * time.Second): t.Fatal("test timed out") } if exitCode != 0 { t.Errorf("expected exit 0 (MkdirAll succeeds), got %d", exitCode) } } func TestCmdExtract_MultipleISOsConcurrent(t *testing.T) { orig := extractISO calls := 0 extractISO = func(isoPath, outDir string) (string, error) { calls++ return fmt.Sprintf("ISO %d done", calls), nil } defer func() { extractISO = orig }() tmp := t.TempDir() isoDir := filepath.Join(tmp, "isos") if err := os.MkdirAll(isoDir, 0755); err != nil { t.Fatal(err) } for _, name := range []string{"iso1.iso", "iso2.iso", "iso3.iso"} { fd, _ := os.Create(filepath.Join(isoDir, name)) if err := fd.Close(); err != nil { t.Fatal(err) } } done := make(chan struct{}) var exitCode int go func() { defer func() { if e := recover(); e != nil { if fe, ok := e.(fatalErr); ok { exitCode = fe.code } } close(done) }() cmdExtract([]string{"-dir", isoDir, "-out", filepath.Join(tmp, "wavs"), "-jobs", "2"}) }() select { case <-done: case <-time.After(5 * time.Second): t.Fatal("test timed out") } if exitCode != 0 { t.Errorf("expected exit 0, got %d", exitCode) } if calls != 3 { t.Errorf("expected 3 extractISO calls, got %d", calls) } } func TestCmdDownload_SingleIdentifier(t *testing.T) { origGet := getItemFiles defer func() { getItemFiles = origGet }() getItemFiles = func(id string) []MetadataFile { return []MetadataFile{ {Name: "test.iso", Size: "1000"}, } } origDL := downloadFile defer func() { downloadFile = origDL }() var dlCalls int downloadFile = func(job downloadJob) (int64, error) { dlCalls++ return job.Size, nil } tmp := t.TempDir() done := make(chan struct{}) var exitCode int go func() { defer func() { if e := recover(); e != nil { if fe, ok := e.(fatalErr); ok { exitCode = fe.code } } close(done) }() cmdDownload([]string{"-i", "testid", "-dir", tmp}) }() select { case <-done: case <-time.After(5 * time.Second): t.Fatal("test timed out") } if exitCode != 0 { t.Errorf("expected exit 0, got %d", exitCode) } if dlCalls != 1 { t.Errorf("expected 1 download call, got %d", dlCalls) } } func TestCmdDownload_NoISOsInItem(t *testing.T) { origGet := getItemFiles defer func() { getItemFiles = origGet }() getItemFiles = func(id string) []MetadataFile { return []MetadataFile{ {Name: "readme.txt", Size: "500"}, } } tmp := t.TempDir() done := make(chan struct{}) var exitCode int go func() { defer func() { if e := recover(); e != nil { if fe, ok := e.(fatalErr); ok { exitCode = fe.code } } close(done) }() cmdDownload([]string{"-i", "n_iso_item", "-dir", tmp}) }() select { case <-done: case <-time.After(3 * time.Second): t.Fatal("test timed out") } if exitCode != 0 { t.Errorf("expected exit 0 (no jobs printed, not fatal), got %d", exitCode) } } func TestCmdDownload_MultipleIdentifiers(t *testing.T) { origGet := getItemFiles defer func() { getItemFiles = origGet }() getItemFiles = func(id string) []MetadataFile { if id == "item1" { return []MetadataFile{{Name: "iso1.iso", Size: "100"}} } if id == "item2" { return []MetadataFile{{Name: "iso2.iso", Size: "200"}} } return nil } origDL := downloadFile defer func() { downloadFile = origDL }() var dlCalls int downloadFile = func(job downloadJob) (int64, error) { dlCalls++ return job.Size, nil } tmp := t.TempDir() idFile := filepath.Join(tmp, "ids.txt") if err := os.WriteFile(idFile, []byte("item1\nitem2\n"), 0644); err != nil { t.Fatal(err) } done := make(chan struct{}) var exitCode int go func() { defer func() { if e := recover(); e != nil { if fe, ok := e.(fatalErr); ok { exitCode = fe.code } } close(done) }() cmdDownload([]string{"-f", idFile, "-dir", tmp}) }() select { case <-done: case <-time.After(5 * time.Second): t.Fatal("test timed out") } if exitCode != 0 { t.Errorf("expected exit 0, got %d", exitCode) } if dlCalls != 2 { t.Errorf("expected 2 download calls, got %d", dlCalls) } } func TestCmdSearch_ServerError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) })) defer server.Close() origSearch := iaSearchURL iaSearchURL = server.URL + "/advancedsearch.php" defer func() { iaSearchURL = origSearch }() orig := osExit var exitCode int osExit = func(code int) { exitCode = code } defer func() { osExit = orig }() done := make(chan struct{}) go func() { defer close(done) cmdSearch([]string{"-query", "test", "-limit", "5"}) }() <-done if exitCode != 1 { t.Errorf("expected exit 1 for server error, got %d", exitCode) } } func TestCmdDownload_DownloadFileError(t *testing.T) { origGet := getItemFiles defer func() { getItemFiles = origGet }() getItemFiles = func(id string) []MetadataFile { return []MetadataFile{ {Name: "test.iso", Size: "1000"}, } } origDL := downloadFile defer func() { downloadFile = origDL }() downloadFile = func(job downloadJob) (int64, error) { return 0, fmt.Errorf("connection reset") } tmp := t.TempDir() done := make(chan struct{}) var exitCode int go func() { defer func() { if e := recover(); e != nil { if fe, ok := e.(fatalErr); ok { exitCode = fe.code } } close(done) }() cmdDownload([]string{"-i", "testid", "-dir", tmp}) }() select { case <-done: case <-time.After(5 * time.Second): t.Fatal("test timed out") } if exitCode != 0 { t.Errorf("expected exit 0 despite download error, got %d", exitCode) } } func TestOpenURL_NoPanic(t *testing.T) { openURL("https://example.com") }