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:
+610
@@ -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")
|
||||
}
|
||||
Reference in New Issue
Block a user