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:
2026-06-21 23:53:00 -07:00
parent 470fb9a4c4
commit 120993fb02
9 changed files with 2130 additions and 29 deletions
+610
View File
@@ -0,0 +1,610 @@
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")
}