cf53912a33
Issue: #9 Backend: - GET /api/list-wavs?dir=<path> — lists .wav files in a directory - GET /wavs/<file> — static file serving from ./output/wavs Web UI: - WAV Browser section with path input + Browse button - AudioContext-based playback (zero deps) — progress bar, time display - WAV file grid with Play buttons, active item highlighting Tests: - 3 new tests for handleListWavs (wav files, empty dir, nonexistent dir) - Fix TestFindAkaiutil: add t.Chdir(tmp) so relative path lookups don't accidentally find the repo's third_party/akaiutil/akaiutil binary
509 lines
13 KiB
Go
509 lines
13 KiB
Go
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_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 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")
|
|
}
|
|
} |