package main import ( "encoding/json" "fmt" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "time" ) func TestWriteJSON(t *testing.T) { t.Run("sets correct headers", func(t *testing.T) { rr := httptest.NewRecorder() writeJSON(rr, map[string]string{"key": "value"}) if ct := rr.Header().Get("Content-Type"); ct != "application/json" { t.Errorf("expected application/json, got %q", ct) } if ac := rr.Header().Get("Access-Control-Allow-Origin"); ac != "*" { t.Errorf("expected *, got %q", ac) } }) t.Run("writes valid JSON", func(t *testing.T) { rr := httptest.NewRecorder() data := map[string]int{"count": 42, "total": 100} writeJSON(rr, data) var result map[string]int if err := json.Unmarshal(rr.Body.Bytes(), &result); err != nil { t.Fatalf("invalid JSON: %v", err) } if result["count"] != 42 { t.Errorf("expected count=42, got %d", result["count"]) } }) t.Run("writes nested structs", func(t *testing.T) { rr := httptest.NewRecorder() type inner struct { Name string `json:"name"` } writeJSON(rr, map[string]any{"item": inner{Name: "test"}}) var result map[string]any if err := json.Unmarshal(rr.Body.Bytes(), &result); err != nil { t.Fatalf("invalid JSON: %v", err) } item := result["item"].(map[string]any) if item["name"] != "test" { t.Errorf("expected name=test, got %v", item["name"]) } }) } func TestHandleProgress_SSEHeaders(t *testing.T) { downloadJobsMu.Lock() downloadJobs = make(map[string]*downloadState) downloadJobs["test-job"] = &downloadState{ ID: "test-job", Identifier: "testid", FileName: "test.iso", TotalSize: 1000, Downloaded: 500, Done: false, } downloadJobsMu.Unlock() req := httptest.NewRequest("GET", "/api/progress", nil) rr := httptest.NewRecorder() done := make(chan string) go func() { handleProgress(rr, req) close(done) }() select { case <-done: case <-time.After(2 * time.Second): } if ct := rr.Header().Get("Content-Type"); ct != "text/event-stream" { t.Errorf("expected text/event-stream, got %q", ct) } if cc := rr.Header().Get("Cache-Control"); cc != "no-cache" { t.Errorf("expected no-cache, got %q", cc) } if conn := rr.Header().Get("Connection"); conn != "keep-alive" { t.Errorf("expected keep-alive, got %q", conn) } } func TestHandleProgress_SSEContent(t *testing.T) { downloadJobsMu.Lock() downloadJobs = make(map[string]*downloadState) downloadJobs["job1"] = &downloadState{ ID: "job1", Identifier: "item1", FileName: "file1.iso", TotalSize: 1000, Downloaded: 1000, Done: true, } downloadJobsMu.Unlock() req := httptest.NewRequest("GET", "/api/progress", nil) rr := httptest.NewRecorder() done := make(chan struct{}) go func() { handleProgress(rr, req) close(done) }() select { case <-done: case <-time.After(2 * time.Second): } body := rr.Body.String() if body == "" { t.Error("expected non-empty SSE body") } body = strings.TrimPrefix(body, "data: ") body = strings.TrimSuffix(strings.TrimSpace(body), "\n\n") var states map[string]*downloadState if err := json.Unmarshal([]byte(body), &states); err != nil { t.Fatalf("invalid JSON in SSE data: %v\nbody: %q", err, body) } if len(states) != 1 { t.Errorf("expected 1 state, got %d", len(states)) } } func TestHandleProgress_EmptyJobs(t *testing.T) { downloadJobsMu.Lock() downloadJobs = make(map[string]*downloadState) downloadJobsMu.Unlock() req := httptest.NewRequest("GET", "/api/progress", nil) rr := httptest.NewRecorder() done := make(chan struct{}) go func() { handleProgress(rr, req) close(done) }() select { case <-done: case <-time.After(2 * time.Second): } body := rr.Body.String() if body == "" { t.Error("expected non-empty SSE body even with no jobs") } } func TestHandleSearch_DelegatesToSearchArchive(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { q := r.URL.Query().Get("q") if q != "(drums) AND (vocal)" { t.Errorf("unexpected query: %q", q) } resp := SearchResult{Response: struct { NumFound int `json:"numFound"` Docs []SearchDoc `json:"docs"` }{ NumFound: 1, Docs: []SearchDoc{ {Identifier: "test-id", Title: "Test", Downloads: 50}, }, }} 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 }() req := httptest.NewRequest("GET", "/api/search?q=drums&filter=vocal", nil) rr := httptest.NewRecorder() handleSearch(rr, req) if rr.Code != http.StatusOK { t.Errorf("expected 200, got %d", rr.Code) } } func TestHandleList_DelegatesToGetItemFiles(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/metadata/testitem" { t.Errorf("unexpected path: %s", r.URL.Path) } resp := MetadataResult{Files: []MetadataFile{ {Name: "test.iso", Size: "1000000"}, }} w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(resp); err != nil { return } })) defer server.Close() origBase := iaBaseURL iaBaseURL = server.URL defer func() { iaBaseURL = origBase }() req := httptest.NewRequest("GET", "/api/list?identifier=testitem", nil) rr := httptest.NewRecorder() handleList(rr, req) if rr.Code != http.StatusOK { t.Errorf("expected 200, got %d", rr.Code) } } func TestHandleList_MissingIdentifier(t *testing.T) { req := httptest.NewRequest("GET", "/api/list", nil) rr := httptest.NewRecorder() handleList(rr, req) if rr.Code != http.StatusBadRequest { t.Errorf("expected 400, got %d", rr.Code) } } func TestHandleAPIDownload_CreatesJobsAndReturnsJSON(t *testing.T) { origDownloadFile := downloadFile defer func() { downloadFile = origDownloadFile }() downloadFile = func(job downloadJob) (int64, error) { downloadJobsMu.Lock() if s, ok := downloadJobs[job.Identifier+"/"+job.Name]; ok { s.Done = true s.Downloaded = job.Size } downloadJobsMu.Unlock() return job.Size, nil } server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { resp := MetadataResult{Files: []MetadataFile{ {Name: "test1.iso", Size: "1000"}, {Name: "test2.iso", Size: "2000"}, }} w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(resp); err != nil { return } })) defer server.Close() origBase := iaBaseURL iaBaseURL = server.URL defer func() { iaBaseURL = origBase }() tmp := t.TempDir() req := httptest.NewRequest("GET", "/api/download?identifier=testitem&dir="+tmp, nil) rr := httptest.NewRecorder() handleAPIDownload(rr, req) if rr.Code != http.StatusOK { t.Errorf("expected 200, got %d: %s", rr.Code, rr.Body.String()) } var resp map[string]any if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil { t.Fatalf("invalid JSON response: %v", err) } jobs, ok := resp["jobs"].([]any) if !ok || len(jobs) != 2 { t.Errorf("expected 2 jobs, got %v", resp["jobs"]) } downloadJobsMu.Lock() defer downloadJobsMu.Unlock() if len(downloadJobs) != 2 { t.Errorf("expected 2 jobs in state map, got %d", len(downloadJobs)) } } func TestHandleAPIDownload_MissingIdentifier(t *testing.T) { req := httptest.NewRequest("GET", "/api/download", nil) rr := httptest.NewRecorder() handleAPIDownload(rr, req) if rr.Code != http.StatusBadRequest { t.Errorf("expected 400, got %d", rr.Code) } } func TestHandleAPIExtract_NoISOs(t *testing.T) { origExtractISO := extractISO defer func() { extractISO = origExtractISO }() extractISO = func(isoPath, outDir string) (string, error) { return "skipped (0 WAVs)", nil } 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, "empty.iso")) defer func() { _ = fd.Close() }() req := httptest.NewRequest("GET", "/api/extract?dir="+isoDir+"&out="+filepath.Join(tmp, "wavs"), nil) rr := httptest.NewRecorder() handleAPIExtract(rr, req) if rr.Code != http.StatusOK { t.Errorf("expected 200, got %d: %s", rr.Code, rr.Body.String()) } var resp map[string]any if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil { t.Fatalf("invalid JSON: %v", err) } if _, ok := resp["error"]; ok { t.Errorf("unexpected error: %v", resp["error"]) } } func TestHandleAPIExtract_WithISOs(t *testing.T) { origExtractISO := extractISO defer func() { extractISO = origExtractISO }() extractISO = func(isoPath, outDir string) (string, error) { return "OK test.iso: Total WAV files: 10 (5s)", nil } 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, "wavs") req := httptest.NewRequest("GET", "/api/extract?dir="+isoDir+"&out="+outDir, nil) rr := httptest.NewRecorder() handleAPIExtract(rr, req) if rr.Code != http.StatusOK { t.Errorf("expected 200, got %d: %s", rr.Code, rr.Body.String()) } } func TestHandleAPIDownload_GetItemFilesReturnsEmpty(t *testing.T) { origGet := getItemFiles defer func() { getItemFiles = origGet }() getItemFiles = func(id string) []MetadataFile { return []MetadataFile{} } tmp := t.TempDir() req := httptest.NewRequest("GET", "/api/download?identifier=testitem&dir="+tmp, nil) rr := httptest.NewRecorder() handleAPIDownload(rr, req) if rr.Code != http.StatusOK { t.Errorf("expected 200, got %d", rr.Code) } var resp map[string]any if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil { t.Fatalf("invalid JSON: %v", err) } if _, ok := resp["error"]; !ok { t.Error("expected error for empty files") } if resp["error"] != "no ISO files found" { t.Errorf("expected 'no ISO files found', got %v", resp["error"]) } } func TestHandleAPIExtract_ReadDirFails(t *testing.T) { tmp := t.TempDir() badDir := filepath.Join(tmp, "nonexistent") req := httptest.NewRequest("GET", "/api/extract?dir="+badDir+"&out="+filepath.Join(tmp, "wavs"), nil) rr := httptest.NewRecorder() handleAPIExtract(rr, req) if rr.Code != http.StatusOK { t.Errorf("expected 200, got %d", rr.Code) } var resp map[string]any if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil { t.Fatalf("invalid JSON: %v", err) } if _, ok := resp["error"]; !ok { t.Error("expected error for failed ReadDir") } } func TestHandleAPIExtract_ExtractISOError(t *testing.T) { origExtractISO := extractISO defer func() { extractISO = origExtractISO }() extractISO = func(isoPath, outDir string) (string, error) { return "", fmt.Errorf("partition read failed") } 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, "bad.iso")) defer func() { _ = fd.Close() }() req := httptest.NewRequest("GET", "/api/extract?dir="+isoDir+"&out="+filepath.Join(tmp, "wavs"), nil) rr := httptest.NewRecorder() handleAPIExtract(rr, req) if rr.Code != http.StatusOK { t.Errorf("expected 200, got %d", rr.Code) } var resp map[string]any if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil { t.Fatalf("invalid JSON: %v", err) } msg, ok := resp["message"].(string) if !ok { t.Fatal("expected message string") } if !strings.Contains(msg, "FAIL bad.iso") { t.Errorf("expected FAIL message, got: %s", msg) } } func TestHandleListWavs_ReturnsWavFiles(t *testing.T) { tmp := t.TempDir() wavDir := filepath.Join(tmp, "wavs") if err := os.MkdirAll(wavDir, 0755); err != nil { t.Fatal(err) } for _, name := range []string{"kick.wav", "snare.wav", "hihat.wav"} { fd, _ := os.Create(filepath.Join(wavDir, name)) _ = fd.Close() } fd, _ := os.Create(filepath.Join(wavDir, "readme.txt")) _ = fd.Close() req := httptest.NewRequest("GET", "/api/list-wavs?dir="+wavDir, nil) rr := httptest.NewRecorder() handleListWavs(rr, req) if rr.Code != http.StatusOK { t.Errorf("expected 200, got %d", rr.Code) } var files []string if err := json.Unmarshal(rr.Body.Bytes(), &files); err != nil { t.Fatalf("invalid JSON: %v", err) } if len(files) != 3 { t.Errorf("expected 3 wav files, got %d: %v", len(files), files) } } func TestHandleListWavs_RecursiveSubdirs(t *testing.T) { tmp := t.TempDir() wavDir := filepath.Join(tmp, "wavs") subDir := filepath.Join(wavDir, "Kicks") if err := os.MkdirAll(subDir, 0755); err != nil { t.Fatal(err) } fd, _ := os.Create(filepath.Join(wavDir, "root.wav")) _ = fd.Close() fd2, _ := os.Create(filepath.Join(subDir, "kick1.wav")) _ = fd2.Close() req := httptest.NewRequest("GET", "/api/list-wavs?dir="+wavDir, nil) rr := httptest.NewRecorder() handleListWavs(rr, req) var files []string if err := json.Unmarshal(rr.Body.Bytes(), &files); err != nil { t.Fatalf("invalid JSON: %v", err) } if len(files) != 2 { t.Errorf("expected 2 wav files, got %d: %v", len(files), files) } found := make(map[string]bool) for _, f := range files { found[f] = true } if !found["root.wav"] { t.Errorf("missing root.wav, got %v", files) } if !found[filepath.Join("Kicks", "kick1.wav")] { t.Errorf("missing Kicks/kick1.wav, got %v", files) } } func TestHandleListWavs_EmptyDir(t *testing.T) { tmp := t.TempDir() req := httptest.NewRequest("GET", "/api/list-wavs?dir="+tmp, nil) rr := httptest.NewRecorder() handleListWavs(rr, req) if rr.Code != http.StatusOK { t.Errorf("expected 200, got %d", rr.Code) } var files []string if err := json.Unmarshal(rr.Body.Bytes(), &files); err != nil { t.Fatalf("invalid JSON: %v", err) } if files == nil { t.Fatal("expected empty array [], got null") } if len(files) != 0 { t.Errorf("expected 0 files, got %d", len(files)) } } func TestHandleListWavs_NonExistentDir(t *testing.T) { req := httptest.NewRequest("GET", "/api/list-wavs?dir=/nonexistent/wavs", nil) rr := httptest.NewRecorder() handleListWavs(rr, req) if rr.Code != http.StatusOK { t.Errorf("expected 200, got %d", rr.Code) } var resp map[string]any if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil { t.Fatalf("invalid JSON: %v", err) } if _, ok := resp["error"]; !ok { t.Error("expected error for nonexistent dir") } }