120993fb02
- 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
610 lines
14 KiB
Go
610 lines
14 KiB
Go
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")
|
|
} |