test: add comprehensive test suite - 5 test files, 65% coverage
- cli_test.go: 10 tests for list/search/extract/download commands - serve_test.go: 11 tests for HTTP handlers and SSE progress - http_test.go: 5 subtests for archive.org API - download_test.go: 10 tests for download logic - util_test.go: 7 tests for string/util helpers Error paths covered: server errors, empty results, download failures, extract failures, missing identifiers, ReadDir errors. golangci-lint: 0 issues, go vet: clean
This commit is contained in:
+445
@@ -0,0 +1,445 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user