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")
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDownloadFile(t *testing.T) {
|
||||
origUserAgent := userAgent
|
||||
userAgent = "akai-fetch-test/1.0"
|
||||
defer func() { userAgent = origUserAgent }()
|
||||
|
||||
t.Run("full download 200 OK", func(t *testing.T) {
|
||||
content := []byte("hello world from archive")
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("User-Agent") != "akai-fetch-test/1.0" {
|
||||
t.Errorf("expected user-agent header, got %q", r.Header.Get("User-Agent"))
|
||||
}
|
||||
if r.Header.Get("Range") != "" {
|
||||
t.Errorf("unexpected Range header on first download: %q", r.Header.Get("Range"))
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if _, err := w.Write(content); err != nil {
|
||||
return
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmp := t.TempDir()
|
||||
outPath := filepath.Join(tmp, "testfile.txt")
|
||||
job := downloadJob{
|
||||
Identifier: "testitem",
|
||||
Name: "testfile.txt",
|
||||
URL: server.URL,
|
||||
OutPath: outPath,
|
||||
Size: int64(len(content)),
|
||||
}
|
||||
|
||||
size, err := downloadFile(job)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if size != int64(len(content)) {
|
||||
t.Errorf("expected %d bytes, got %d", len(content), size)
|
||||
}
|
||||
data, _ := os.ReadFile(outPath)
|
||||
if string(data) != string(content) {
|
||||
t.Errorf("file content mismatch: got %q", string(data))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("resume partial download 206 Partial Content", func(t *testing.T) {
|
||||
existingContent := []byte("content file here123456789012345")
|
||||
newContent := []byte(" appended data")
|
||||
existingLen := int64(len(existingContent))
|
||||
totalLen := existingLen + int64(len(newContent))
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
rangeH := r.Header.Get("Range")
|
||||
if rangeH == "" {
|
||||
t.Error("expected Range header for resume")
|
||||
}
|
||||
expectedRange := "bytes=" + strconv.FormatInt(existingLen, 10) + "-"
|
||||
if rangeH != expectedRange {
|
||||
t.Errorf("expected Range %q, got %q", expectedRange, rangeH)
|
||||
}
|
||||
w.WriteHeader(http.StatusPartialContent)
|
||||
if _, err := w.Write(newContent); err != nil {
|
||||
return
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmp := t.TempDir()
|
||||
outPath := filepath.Join(tmp, "partial.txt")
|
||||
if err := os.WriteFile(outPath, existingContent, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
job := downloadJob{
|
||||
Identifier: "testitem",
|
||||
Name: "partial.txt",
|
||||
URL: server.URL,
|
||||
OutPath: outPath,
|
||||
Size: totalLen,
|
||||
}
|
||||
|
||||
size, err := downloadFile(job)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if size != totalLen {
|
||||
t.Errorf("expected %d total bytes, got %d", totalLen, size)
|
||||
}
|
||||
data, _ := os.ReadFile(outPath)
|
||||
if string(data) != string(existingContent)+string(newContent) {
|
||||
t.Errorf("file content mismatch, got %q", string(data))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("skip when file already complete", func(t *testing.T) {
|
||||
calls := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
calls++
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if _, err := w.Write([]byte("should not be called")); err != nil {
|
||||
return
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmp := t.TempDir()
|
||||
outPath := filepath.Join(tmp, "complete.txt")
|
||||
existingContent := []byte("already complete file content here")
|
||||
if err := os.WriteFile(outPath, existingContent, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
job := downloadJob{
|
||||
Identifier: "testitem",
|
||||
Name: "complete.txt",
|
||||
URL: server.URL,
|
||||
OutPath: outPath,
|
||||
Size: int64(len(existingContent)),
|
||||
}
|
||||
|
||||
size, err := downloadFile(job)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if size != int64(len(existingContent)) {
|
||||
t.Errorf("expected %d bytes, got %d", len(existingContent), size)
|
||||
}
|
||||
if calls > 0 {
|
||||
t.Errorf("server should not have been called, got %d calls", calls)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("HTTP error returns error", func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmp := t.TempDir()
|
||||
outPath := filepath.Join(tmp, "notfound.txt")
|
||||
job := downloadJob{
|
||||
Identifier: "testitem",
|
||||
Name: "notfound.txt",
|
||||
URL: server.URL,
|
||||
OutPath: outPath,
|
||||
Size: 1234,
|
||||
}
|
||||
|
||||
_, err := downloadFile(job)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for 404 response")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("HTTP 500 returns error", func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmp := t.TempDir()
|
||||
outPath := filepath.Join(tmp, "error.txt")
|
||||
job := downloadJob{
|
||||
Identifier: "testitem",
|
||||
Name: "error.txt",
|
||||
URL: server.URL,
|
||||
OutPath: outPath,
|
||||
Size: 999,
|
||||
}
|
||||
|
||||
_, err := downloadFile(job)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for 500 response")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("download creates parent directories", func(t *testing.T) {
|
||||
content := []byte("content")
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if _, err := w.Write(content); err != nil {
|
||||
return
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmp := t.TempDir()
|
||||
outPath := filepath.Join(tmp, "nested", "dirs", "file.txt")
|
||||
job := downloadJob{
|
||||
Identifier: "testitem",
|
||||
Name: "file.txt",
|
||||
URL: server.URL,
|
||||
OutPath: outPath,
|
||||
Size: int64(len(content)),
|
||||
}
|
||||
|
||||
size, err := downloadFile(job)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if size != int64(len(content)) {
|
||||
t.Errorf("expected %d bytes, got %d", len(content), size)
|
||||
}
|
||||
if _, err := os.Stat(outPath); err != nil {
|
||||
t.Errorf("file should exist at nested path: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("zero size file downloads without size check", func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmp := t.TempDir()
|
||||
outPath := filepath.Join(tmp, "zerofile")
|
||||
job := downloadJob{
|
||||
Identifier: "testitem",
|
||||
Name: "zerofile",
|
||||
URL: server.URL,
|
||||
OutPath: outPath,
|
||||
Size: 0,
|
||||
}
|
||||
|
||||
size, err := downloadFile(job)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if size != 0 {
|
||||
t.Errorf("expected 0 bytes, got %d", size)
|
||||
}
|
||||
data, _ := os.ReadFile(outPath)
|
||||
if len(data) != 0 {
|
||||
t.Errorf("expected empty file, got %d bytes", len(data))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("partial file without size match triggers full resume", func(t *testing.T) {
|
||||
existingContent := []byte("partial but wrong size")
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
rangeH := r.Header.Get("Range")
|
||||
if rangeH == "" {
|
||||
t.Error("expected Range header")
|
||||
}
|
||||
w.WriteHeader(http.StatusPartialContent)
|
||||
if _, err := w.Write([]byte(" more data")); err != nil {
|
||||
return
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmp := t.TempDir()
|
||||
outPath := filepath.Join(tmp, "partial.txt")
|
||||
if err := os.WriteFile(outPath, existingContent, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
job := downloadJob{
|
||||
Identifier: "testitem",
|
||||
Name: "partial.txt",
|
||||
URL: server.URL,
|
||||
OutPath: outPath,
|
||||
Size: int64(len(existingContent) + 100), // wrong size, triggers resume
|
||||
}
|
||||
|
||||
_, err := downloadFile(job)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDownloadFile_ClientErrors(t *testing.T) {
|
||||
t.Run("connection refused returns error", func(t *testing.T) {
|
||||
job := downloadJob{
|
||||
URL: "http://127.0.0.1:1/notexist",
|
||||
OutPath: "/tmp/out.txt",
|
||||
Size: 100,
|
||||
}
|
||||
_, err := downloadFile(job)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for connection refused")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid URL returns error", func(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
job := downloadJob{
|
||||
URL: "://invalid",
|
||||
OutPath: filepath.Join(tmp, "out.txt"),
|
||||
Size: 100,
|
||||
}
|
||||
_, err := downloadFile(job)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for invalid URL")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestHumanSize_EdgeCases(t *testing.T) {
|
||||
cases := []struct {
|
||||
bytes int64
|
||||
expect string
|
||||
}{
|
||||
{1, "1B"},
|
||||
{1023, "1023B"},
|
||||
{1048577, "1.0 MB"}, // just over 1 MiB
|
||||
{1073741824 * 4, "4.0 GB"},
|
||||
{1099511627776, "1.0 TB"}, // 1 TiB
|
||||
}
|
||||
for _, c := range cases {
|
||||
got := humanSize(c.bytes)
|
||||
if got != c.expect {
|
||||
t.Errorf("humanSize(%d) = %q, want %q", c.bytes, got, c.expect)
|
||||
}
|
||||
}
|
||||
}
|
||||
+17
-7
@@ -55,11 +55,17 @@ var runAkaiutil = func(isoPath, commands string) (string, error) {
|
||||
return "", fmt.Errorf("start akaiutil: %w", err)
|
||||
}
|
||||
|
||||
stdin.Write([]byte(commands))
|
||||
stdin.Close()
|
||||
if _, err := stdin.Write([]byte(commands)); err != nil {
|
||||
return "", fmt.Errorf("write stdin: %w", err)
|
||||
}
|
||||
if err := stdin.Close(); err != nil {
|
||||
return "", fmt.Errorf("close stdin: %w", err)
|
||||
}
|
||||
|
||||
output, _ := io.ReadAll(stdout)
|
||||
cmd.Wait()
|
||||
if err := cmd.Wait(); err != nil {
|
||||
return "", fmt.Errorf("wait akaiutil: %w", err)
|
||||
}
|
||||
|
||||
return string(output), nil
|
||||
}
|
||||
@@ -130,7 +136,9 @@ func runExtraction(isoPath, baseDir string, vols []volInfo) (int, error) {
|
||||
safe = "volume"
|
||||
}
|
||||
volDir := filepath.Join(baseDir, safe)
|
||||
os.MkdirAll(volDir, 0755)
|
||||
if err := os.MkdirAll(volDir, 0755); err != nil {
|
||||
return 0, fmt.Errorf("mkdir %s: %w", volDir, err)
|
||||
}
|
||||
|
||||
nav := strings.ReplaceAll(v.Name, " ", "_")
|
||||
fmt.Fprintf(&cmds, "lcd %s\n", volDir)
|
||||
@@ -171,17 +179,19 @@ func extractWAVs(isoPath, outDir string) (int, error) {
|
||||
|
||||
isoBase := sanitiseISOName(filepath.Base(isoPath))
|
||||
baseDir := filepath.Join(outDir, isoBase)
|
||||
os.MkdirAll(baseDir, 0755)
|
||||
if err := os.MkdirAll(baseDir, 0755); err != nil {
|
||||
return 0, fmt.Errorf("mkdir %s: %w", baseDir, err)
|
||||
}
|
||||
|
||||
return runExtraction(isoPath, baseDir, vols)
|
||||
}
|
||||
|
||||
func extractISO(isoPath, outDir string) (string, error) {
|
||||
var extractISO = func(isoPath, outDir string) (string, error) {
|
||||
isoOutDir := filepath.Join(outDir, sanitiseISOName(filepath.Base(isoPath)))
|
||||
|
||||
if fi, err := os.Stat(isoOutDir); err == nil && fi.IsDir() {
|
||||
var wavCount int
|
||||
filepath.WalkDir(isoOutDir, func(p string, d os.DirEntry, err error) error {
|
||||
_ = filepath.WalkDir(isoOutDir, func(p string, d os.DirEntry, err error) error {
|
||||
if err == nil && !d.IsDir() && strings.HasSuffix(strings.ToLower(d.Name()), ".wav") {
|
||||
wavCount++
|
||||
}
|
||||
|
||||
+253
@@ -1,9 +1,78 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestExtractISO_SkipsExistingWAVs(t *testing.T) {
|
||||
orig := runAkaiutil
|
||||
defer func() { runAkaiutil = orig }()
|
||||
|
||||
called := false
|
||||
runAkaiutil = func(isoPath, commands string) (string, error) {
|
||||
called = true
|
||||
return "", nil
|
||||
}
|
||||
|
||||
tmp := t.TempDir()
|
||||
isoOutDir := filepath.Join(tmp, sanitiseISOName(filepath.Base("/fake/test.iso")))
|
||||
if err := os.MkdirAll(isoOutDir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, name := range []string{"kick.wav", "snare.wav", "hihat.wav"} {
|
||||
fd, err := os.Create(filepath.Join(isoOutDir, name))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() { _ = fd.Close() }()
|
||||
}
|
||||
|
||||
result, err := extractISO("/fake/test.iso", tmp)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if called {
|
||||
t.Fatal("runAkaiutil should not have been called for existing WAVs")
|
||||
}
|
||||
if result == "" {
|
||||
t.Fatal("expected non-empty result")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractISO_ProceedsWhenNoWAVs(t *testing.T) {
|
||||
orig := runAkaiutil
|
||||
defer func() { runAkaiutil = orig }()
|
||||
|
||||
calls := 0
|
||||
runAkaiutil = func(isoPath, commands string) (string, error) {
|
||||
calls++
|
||||
if commands == "df\nq\n" {
|
||||
return `
|
||||
Disk /disk0
|
||||
A - S1000 HD (3840 blocks)
|
||||
`, nil
|
||||
}
|
||||
return ` exported 3 file(s)
|
||||
`, nil
|
||||
}
|
||||
|
||||
tmp := t.TempDir()
|
||||
result, err := extractISO("/fake/test.iso", tmp)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if calls == 0 {
|
||||
t.Fatal("runAkaiutil should have been called")
|
||||
}
|
||||
if result == "" {
|
||||
t.Fatal("expected non-empty result")
|
||||
}
|
||||
}
|
||||
|
||||
func TestListPartitions(t *testing.T) {
|
||||
orig := runAkaiutil
|
||||
defer func() { runAkaiutil = orig }()
|
||||
@@ -115,3 +184,187 @@ func TestRunExtractionCount(t *testing.T) {
|
||||
t.Fatalf("expected 1 akaiutil call, got %d", calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractWAVs_Success(t *testing.T) {
|
||||
orig := runAkaiutil
|
||||
defer func() { runAkaiutil = orig }()
|
||||
|
||||
callCount := 0
|
||||
runAkaiutil = func(isoPath, commands string) (string, error) {
|
||||
callCount++
|
||||
if callCount == 1 {
|
||||
if commands != "df\nq\n" {
|
||||
t.Fatalf("first call: expected df\\q\\n, got %q", commands)
|
||||
}
|
||||
return `
|
||||
Disk /disk0
|
||||
A - S1000 HD (3840 blocks)
|
||||
`, nil
|
||||
}
|
||||
if callCount == 2 {
|
||||
return `
|
||||
/disk0/A > cd /disk0/A
|
||||
/disk0/A > dir
|
||||
1 DRUMS - VOLUME (10 files)
|
||||
/disk0/A > q
|
||||
`, nil
|
||||
}
|
||||
if callCount == 3 {
|
||||
return ` exported 10 file(s)
|
||||
`, nil
|
||||
}
|
||||
t.Fatalf("unexpected call #%d with commands: %q", callCount, commands)
|
||||
return "", nil
|
||||
}
|
||||
|
||||
tmp := t.TempDir()
|
||||
total, err := extractWAVs("/fake/test.iso", tmp)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if total != 10 {
|
||||
t.Fatalf("expected 10 WAVs, got %d", total)
|
||||
}
|
||||
if callCount != 3 {
|
||||
t.Fatalf("expected 3 akaiutil calls, got %d", callCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractWAVs_MultipleVolumes(t *testing.T) {
|
||||
orig := runAkaiutil
|
||||
defer func() { runAkaiutil = orig }()
|
||||
|
||||
calls := 0
|
||||
runAkaiutil = func(isoPath, commands string) (string, error) {
|
||||
calls++
|
||||
if calls == 1 {
|
||||
return `
|
||||
Disk /disk0
|
||||
A - S1000 HD (3840 blocks)
|
||||
B - S1000 HD (5120 blocks)
|
||||
`, nil
|
||||
}
|
||||
if calls == 2 {
|
||||
return `
|
||||
/disk0/A > cd /disk0/A
|
||||
1 VOL_A - VOLUME (5 files)
|
||||
/disk0/B > cd /disk0/B
|
||||
2 VOL_B - VOLUME (8 files)
|
||||
`, nil
|
||||
}
|
||||
return ` exported 5 file(s)
|
||||
exported 8 file(s)
|
||||
`, nil
|
||||
}
|
||||
|
||||
tmp := t.TempDir()
|
||||
total, err := extractWAVs("/fake/multivol.iso", tmp)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if total != 13 {
|
||||
t.Fatalf("expected 13 WAVs, got %d", total)
|
||||
}
|
||||
if calls != 3 {
|
||||
t.Fatalf("expected 3 calls, got %d", calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractWAVs_NoVolumes(t *testing.T) {
|
||||
orig := runAkaiutil
|
||||
defer func() { runAkaiutil = orig }()
|
||||
|
||||
calls := 0
|
||||
runAkaiutil = func(isoPath, commands string) (string, error) {
|
||||
calls++
|
||||
if calls == 1 {
|
||||
return `Disk /disk0
|
||||
A - S1000 HD (3840 blocks)
|
||||
`, nil
|
||||
}
|
||||
if calls == 2 {
|
||||
return `/disk0/A > cd /disk0/A
|
||||
/disk0/A > q
|
||||
`, nil
|
||||
}
|
||||
return ` exported 0 file(s)
|
||||
`, nil
|
||||
}
|
||||
|
||||
tmp := t.TempDir()
|
||||
total, err := extractWAVs("/fake/empty.iso", tmp)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if total != 0 {
|
||||
t.Fatalf("expected 0 WAVs, got %d", total)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractWAVs_ErrorInPartitionList(t *testing.T) {
|
||||
orig := runAkaiutil
|
||||
defer func() { runAkaiutil = orig }()
|
||||
|
||||
runAkaiutil = func(isoPath, commands string) (string, error) {
|
||||
return "", fmt.Errorf("simulated partition error")
|
||||
}
|
||||
|
||||
tmp := t.TempDir()
|
||||
_, err := extractWAVs("/fake/error.iso", tmp)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractWAVs_ErrorInVolumeList(t *testing.T) {
|
||||
orig := runAkaiutil
|
||||
defer func() { runAkaiutil = orig }()
|
||||
|
||||
calls := 0
|
||||
runAkaiutil = func(isoPath, commands string) (string, error) {
|
||||
calls++
|
||||
if calls == 1 {
|
||||
return `
|
||||
Disk /disk0
|
||||
A - S1000 HD (3840 blocks)
|
||||
`, nil
|
||||
}
|
||||
return "", fmt.Errorf("simulated volume list error")
|
||||
}
|
||||
|
||||
tmp := t.TempDir()
|
||||
_, err := extractWAVs("/fake/error.iso", tmp)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunExtraction_CreatesVolumeDirs(t *testing.T) {
|
||||
orig := runAkaiutil
|
||||
defer func() { runAkaiutil = orig }()
|
||||
|
||||
created := map[string]bool{}
|
||||
runAkaiutil = func(isoPath, commands string) (string, error) {
|
||||
for _, line := range strings.Split(commands, "\n") {
|
||||
if strings.HasPrefix(line, "lcd ") {
|
||||
dir := strings.TrimPrefix(line, "lcd ")
|
||||
created[dir] = true
|
||||
}
|
||||
}
|
||||
return ` exported 3 file(s)
|
||||
`, nil
|
||||
}
|
||||
|
||||
tmp := t.TempDir()
|
||||
vols := []volInfo{
|
||||
{Part: "A", Name: "Test Volume One"},
|
||||
}
|
||||
baseDir := filepath.Join(tmp, "output")
|
||||
_, err := runExtraction("/fake/test.iso", baseDir, vols)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if !created["/"] {
|
||||
t.Error("expected lcd / to be in commands")
|
||||
}
|
||||
}
|
||||
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetItemFiles(t *testing.T) {
|
||||
t.Run("returns ISO files with sizes", func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
expectedPath := "/metadata/testitem"
|
||||
if r.URL.Path != expectedPath {
|
||||
t.Errorf("expected path %q, got %q", expectedPath, r.URL.Path)
|
||||
}
|
||||
resp := MetadataResult{
|
||||
Files: []MetadataFile{
|
||||
{Name: "folder/something.txt", Size: "1234", Format: "text"},
|
||||
{Name: "synth.iso", Size: "524288000", Format: "CD image"},
|
||||
{Name: "drums.iso", Size: "1048576", Format: "CD image"},
|
||||
{Name: "README.txt", Size: "500", Format: "text"},
|
||||
},
|
||||
}
|
||||
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 }()
|
||||
|
||||
files := getItemFiles("testitem")
|
||||
if len(files) != 2 {
|
||||
t.Fatalf("expected 2 ISO files, got %d: %+v", len(files), files)
|
||||
}
|
||||
if files[0].Name != "synth.iso" || files[0].Size != "524288000" {
|
||||
t.Errorf("unexpected first file: %+v", files[0])
|
||||
}
|
||||
if files[1].Name != "drums.iso" || files[1].Size != "1048576" {
|
||||
t.Errorf("unexpected second file: %+v", files[1])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no ISO files returns empty", func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
resp := MetadataResult{Files: []MetadataFile{
|
||||
{Name: "readme.txt", Size: "100", Format: "text"},
|
||||
{Name: "cover.jpg", Size: "50000", Format: "image"},
|
||||
}}
|
||||
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 }()
|
||||
|
||||
files := getItemFiles("noisos")
|
||||
if len(files) != 0 {
|
||||
t.Fatalf("expected 0 files, got %d", len(files))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("server returns 500", func(t *testing.T) {
|
||||
calls := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
calls++
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
origBase := iaBaseURL
|
||||
iaBaseURL = server.URL
|
||||
defer func() { iaBaseURL = origBase }()
|
||||
|
||||
// Verify server is called and returns 500
|
||||
if calls != 0 {
|
||||
t.Errorf("expected 0 calls before getItemFiles, got %d", calls)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSearchArchive(t *testing.T) {
|
||||
t.Run("parses search results correctly", func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query().Get("q")
|
||||
if q != "subject:akai AND subject:sampler" {
|
||||
t.Errorf("unexpected query: %q", q)
|
||||
}
|
||||
resp := SearchResult{
|
||||
Response: struct {
|
||||
NumFound int `json:"numFound"`
|
||||
Docs []SearchDoc `json:"docs"`
|
||||
}{
|
||||
NumFound: 2,
|
||||
Docs: []SearchDoc{
|
||||
{Identifier: "akai-drums-vol1", Title: "AKAI Drums Vol 1", Downloads: 500, Format: []string{"CD image"}},
|
||||
{Identifier: "akai-synth-pads", Title: "AKAI Synth Pads", Downloads: 200, Format: []string{"CD image"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
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 }()
|
||||
|
||||
results := searchArchive("subject:akai AND subject:sampler", 30)
|
||||
if len(results) != 2 {
|
||||
t.Fatalf("expected 2 results, got %d", len(results))
|
||||
}
|
||||
if results[0].Identifier != "akai-drums-vol1" || results[0].Downloads != 500 {
|
||||
t.Errorf("unexpected first result: %+v", results[0])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty results returns empty slice", func(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 }()
|
||||
|
||||
results := searchArchive("nonexistent query", 30)
|
||||
if len(results) != 0 {
|
||||
t.Fatalf("expected 0 results, got %d", len(results))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("URL encoding of query", func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
q, _ := url.QueryUnescape(r.URL.RawQuery)
|
||||
if !containsQueryParam(q, "vocal") {
|
||||
t.Errorf("query should contain 'vocal': %s", q)
|
||||
}
|
||||
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 }()
|
||||
|
||||
searchArchive("vocal", 10)
|
||||
})
|
||||
}
|
||||
|
||||
func containsQueryParam(query string, substr string) bool {
|
||||
for _, pair := range strings.Split(query, "&") {
|
||||
if strings.Contains(pair, substr) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -16,10 +16,10 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
var (
|
||||
userAgent = "akai-fetch/1.0"
|
||||
iaSearchURL = "https://archive.org/advancedsearch.php"
|
||||
iaBaseURL = "https://archive.org"
|
||||
userAgent = "akai-fetch/1.0"
|
||||
)
|
||||
|
||||
type SearchResult struct {
|
||||
@@ -121,6 +121,15 @@ Serve flags:
|
||||
// ─── Search ────────────────────────────────────────────────────────────
|
||||
|
||||
func cmdSearch(args []string) {
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
if fe, ok := e.(fatalErr); ok {
|
||||
osExit(fe.code)
|
||||
return
|
||||
}
|
||||
panic(e)
|
||||
}
|
||||
}()
|
||||
fs := flag.NewFlagSet("search", flag.ExitOnError)
|
||||
query := fs.String("query", "subject:akai AND subject:sampler", "search query")
|
||||
filter := fs.String("filter", "", "extra term AND-ed onto the query")
|
||||
@@ -167,7 +176,7 @@ func searchArchive(query string, limit int) []SearchDoc {
|
||||
if err != nil {
|
||||
fatal("search request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
@@ -184,6 +193,15 @@ func searchArchive(query string, limit int) []SearchDoc {
|
||||
// ─── List ──────────────────────────────────────────────────────────────
|
||||
|
||||
func cmdList(args []string) {
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
if fe, ok := e.(fatalErr); ok {
|
||||
osExit(fe.code)
|
||||
return
|
||||
}
|
||||
panic(e)
|
||||
}
|
||||
}()
|
||||
if len(args) < 1 {
|
||||
fatal("usage: akai-fetch list <identifier>")
|
||||
}
|
||||
@@ -200,7 +218,7 @@ func cmdList(args []string) {
|
||||
}
|
||||
}
|
||||
|
||||
func getItemFiles(identifier string) []MetadataFile {
|
||||
var getItemFiles = func(identifier string) []MetadataFile {
|
||||
u := fmt.Sprintf("%s/metadata/%s", iaBaseURL, url.PathEscape(identifier))
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
req, _ := http.NewRequest("GET", u, nil)
|
||||
@@ -209,7 +227,7 @@ func getItemFiles(identifier string) []MetadataFile {
|
||||
if err != nil {
|
||||
fatal("metadata request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
@@ -243,6 +261,15 @@ type downloadJob struct {
|
||||
}
|
||||
|
||||
func cmdDownload(args []string) {
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
if fe, ok := e.(fatalErr); ok {
|
||||
osExit(fe.code)
|
||||
return
|
||||
}
|
||||
panic(e)
|
||||
}
|
||||
}()
|
||||
fs := flag.NewFlagSet("download", flag.ExitOnError)
|
||||
identifier := fs.String("i", "", "archive.org item identifier")
|
||||
idFile := fs.String("f", "", "file with identifiers (one per line)")
|
||||
@@ -329,8 +356,10 @@ func cmdDownload(args []string) {
|
||||
fmt.Printf("\nDownloaded %s total\n", humanSize(totalDownloaded))
|
||||
}
|
||||
|
||||
func downloadFile(job downloadJob) (int64, error) {
|
||||
os.MkdirAll(filepath.Dir(job.OutPath), 0755)
|
||||
var downloadFile = func(job downloadJob) (int64, error) {
|
||||
if err := os.MkdirAll(filepath.Dir(job.OutPath), 0755); err != nil {
|
||||
return 0, fmt.Errorf("mkdir: %w", err)
|
||||
}
|
||||
|
||||
// Check if file already exists and has the expected size
|
||||
if fi, err := os.Stat(job.OutPath); err == nil {
|
||||
@@ -359,7 +388,7 @@ func downloadFile(job downloadJob) (int64, error) {
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("get: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusPartialContent {
|
||||
return 0, fmt.Errorf("HTTP %d", resp.StatusCode)
|
||||
@@ -369,7 +398,7 @@ func downloadFile(job downloadJob) (int64, error) {
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("open: %w", err)
|
||||
}
|
||||
defer outFile.Close()
|
||||
defer func() { _ = outFile.Close() }()
|
||||
|
||||
written, err := io.Copy(outFile, resp.Body)
|
||||
if err != nil {
|
||||
@@ -381,6 +410,15 @@ func downloadFile(job downloadJob) (int64, error) {
|
||||
// ─── Extract ───────────────────────────────────────────────────────────
|
||||
|
||||
func cmdExtract(args []string) {
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
if fe, ok := e.(fatalErr); ok {
|
||||
osExit(fe.code)
|
||||
return
|
||||
}
|
||||
panic(e)
|
||||
}
|
||||
}()
|
||||
fs := flag.NewFlagSet("extract", flag.ExitOnError)
|
||||
isoDir := fs.String("dir", ".", "directory with ISO files")
|
||||
outDir := fs.String("out", "./wavs", "output directory for WAVs")
|
||||
@@ -462,6 +500,15 @@ func sanitiseISOName(path string) string {
|
||||
// ─── Pipeline ──────────────────────────────────────────────────────────
|
||||
|
||||
func cmdPipeline(args []string) {
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
if fe, ok := e.(fatalErr); ok {
|
||||
osExit(fe.code)
|
||||
return
|
||||
}
|
||||
panic(e)
|
||||
}
|
||||
}()
|
||||
fs := flag.NewFlagSet("pipeline", flag.ExitOnError)
|
||||
query := fs.String("query", "subject:akai AND subject:sampler", "search query")
|
||||
filter := fs.String("filter", "", "extra term AND-ed onto the query")
|
||||
@@ -527,9 +574,17 @@ func cmdPipeline(args []string) {
|
||||
|
||||
// ─── Helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
var osExit = os.Exit
|
||||
|
||||
type fatalErr struct {
|
||||
code int
|
||||
message string
|
||||
}
|
||||
|
||||
func fatal(format string, args ...any) {
|
||||
fmt.Fprintf(os.Stderr, "error: "+format+"\n", args...)
|
||||
os.Exit(1)
|
||||
msg := fmt.Sprintf("error: "+format+"\n", args...)
|
||||
fmt.Fprintf(os.Stderr, msg, args...)
|
||||
panic(fatalErr{code: 1, message: msg})
|
||||
}
|
||||
|
||||
func truncate(s string, n int) string {
|
||||
@@ -556,6 +611,8 @@ func humanSize(bytes int64) string {
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Ensure we can parse flags even with -count or other test flags
|
||||
flag.CommandLine.Parse([]string{})
|
||||
if err := flag.CommandLine.Parse([]string{}); err != nil {
|
||||
// In init context, best we can do is note the issue
|
||||
fmt.Fprintf(os.Stderr, "flag init: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,7 +189,10 @@ func handleAPIDownload(w http.ResponseWriter, r *http.Request) {
|
||||
jobIDs = append(jobIDs, jobID)
|
||||
|
||||
go func(j downloadJob, s *downloadState) {
|
||||
os.MkdirAll(filepath.Dir(j.OutPath), 0755)
|
||||
if err := os.MkdirAll(filepath.Dir(j.OutPath), 0755); err != nil {
|
||||
s.Error = err.Error()
|
||||
return
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 0}
|
||||
req, _ := http.NewRequest("GET", j.URL, nil)
|
||||
@@ -206,20 +209,23 @@ func handleAPIDownload(w http.ResponseWriter, r *http.Request) {
|
||||
s.Error = err.Error()
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
outFile, err := os.Create(j.OutPath)
|
||||
if err != nil {
|
||||
s.Error = err.Error()
|
||||
return
|
||||
}
|
||||
defer outFile.Close()
|
||||
defer func() { _ = outFile.Close() }()
|
||||
|
||||
buf := make([]byte, 32*1024)
|
||||
for {
|
||||
n, readErr := resp.Body.Read(buf)
|
||||
if n > 0 {
|
||||
outFile.Write(buf[:n])
|
||||
if _, err := outFile.Write(buf[:n]); err != nil {
|
||||
s.Error = err.Error()
|
||||
return
|
||||
}
|
||||
s.Downloaded += int64(n)
|
||||
|
||||
now := time.Now()
|
||||
@@ -267,7 +273,9 @@ func handleProgress(w http.ResponseWriter, r *http.Request) {
|
||||
data, _ := json.Marshal(downloadJobs)
|
||||
downloadJobsMu.Unlock()
|
||||
|
||||
fmt.Fprintf(w, "data: %s\n\n", data)
|
||||
if _, err := fmt.Fprintf(w, "data: %s\n\n", data); err != nil {
|
||||
return
|
||||
}
|
||||
flusher.Flush()
|
||||
|
||||
allDone := true
|
||||
@@ -331,13 +339,16 @@ func handleAPIExtract(w http.ResponseWriter, r *http.Request) {
|
||||
func writeJSON(w http.ResponseWriter, v any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
json.NewEncoder(w).Encode(v)
|
||||
if err := json.NewEncoder(w).Encode(v); err != nil {
|
||||
// client disconnected, nothing we can do
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func openURL(rawURL string) {
|
||||
exec.Command("xdg-open", rawURL).Start()
|
||||
exec.Command("open", rawURL).Start()
|
||||
exec.Command("rundll32", "url.dll,FileProtocolHandler", rawURL).Start()
|
||||
_ = exec.Command("xdg-open", rawURL).Start()
|
||||
_ = exec.Command("open", rawURL).Start()
|
||||
_ = exec.Command("rundll32", "url.dll,FileProtocolHandler", rawURL).Start()
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
||||
+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)
|
||||
}
|
||||
}
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSanitiseISOName(t *testing.T) {
|
||||
cases := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"ZERO-G - Deepest India", "ZERO-G_-_Deepest_India"},
|
||||
{"SYNTH PAD COLLECTION.iso", "SYNTH_PAD_COLLECTION"},
|
||||
{"vocal.loops", "vocal"},
|
||||
{"DRUM KIT #1", "DRUM_KIT_1"},
|
||||
{"simple", "simple"},
|
||||
{"no-extension", "no-extension"},
|
||||
{"multiple spaces", "multiple_spaces"},
|
||||
{"UPPERCASE", "UPPERCASE"},
|
||||
{"MiXeD CaSe", "MiXeD_CaSe"},
|
||||
{"with-dash", "with-dash"},
|
||||
{"with_underscore", "with_underscore"},
|
||||
{"with.dots.in.name", "with.dots.in"},
|
||||
{".hidden", ""},
|
||||
{"trailing_", "trailing"},
|
||||
{"__leading__", "leading"},
|
||||
{"spaces everywhere", "spaces_everywhere"},
|
||||
{"", ""},
|
||||
{" ", ""},
|
||||
{".iso", ""},
|
||||
{"a", "a"},
|
||||
}
|
||||
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 TestTruncate(t *testing.T) {
|
||||
cases := []struct {
|
||||
s string
|
||||
n int
|
||||
expect string
|
||||
}{
|
||||
{"hello", 10, "hello"},
|
||||
{"hello", 5, "hello"},
|
||||
{"hello", 4, "h..."},
|
||||
{"hello", 3, "..."},
|
||||
{"hello", 2, "he"},
|
||||
{"hello", 1, "h"},
|
||||
{"hello", 0, ""},
|
||||
{"", 5, ""},
|
||||
{"abcdef", 4, "a..."},
|
||||
{"ab", 4, "ab"},
|
||||
{"abc", 3, "abc"},
|
||||
{"abcd", 2, "ab"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
got := truncate(c.s, c.n)
|
||||
if got != c.expect {
|
||||
t.Errorf("truncate(%q, %d) = %q, want %q", c.s, c.n, got, c.expect)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHumanSize(t *testing.T) {
|
||||
cases := []struct {
|
||||
bytes int64
|
||||
expect string
|
||||
}{
|
||||
{0, "0B"},
|
||||
{500, "500B"},
|
||||
{1024, "1.0 KB"},
|
||||
{1536, "1.5 KB"},
|
||||
{10240, "10.0 KB"},
|
||||
{1048576, "1.0 MB"},
|
||||
{1572864, "1.5 MB"},
|
||||
{1073741824, "1.0 GB"},
|
||||
{2147483648, "2.0 GB"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
got := humanSize(c.bytes)
|
||||
if got != c.expect {
|
||||
t.Errorf("humanSize(%d) = %q, want %q", c.bytes, got, c.expect)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindAkaiutil(t *testing.T) {
|
||||
origEnv := os.Getenv("AKAIUTIL")
|
||||
origArg0 := os.Args[0]
|
||||
defer func() {
|
||||
_ = os.Setenv("AKAIUTIL", origEnv)
|
||||
os.Args[0] = origArg0
|
||||
}()
|
||||
|
||||
t.Run("env var set to missing file", func(t *testing.T) {
|
||||
_ = os.Setenv("AKAIUTIL", "/nonexistent/akaiutil")
|
||||
os.Args[0] = "/some/binary"
|
||||
_, err := findAkaiutil()
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing env var path")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("env var set to existing file", func(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
f := tmp + "/myutil"
|
||||
fd, _ := os.Create(f)
|
||||
defer func() { _ = fd.Close() }()
|
||||
_ = os.Setenv("AKAIUTIL", f)
|
||||
got, err := findAkaiutil()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if got != f {
|
||||
t.Errorf("got %q, want %q", got, f)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("finds binary next to argv[0]", func(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
exe := tmp + "/mybinary"
|
||||
fd, _ := os.Create(exe)
|
||||
defer func() { _ = fd.Close() }()
|
||||
sideBySide := tmp + "/akaiutil"
|
||||
fd2, _ := os.Create(sideBySide)
|
||||
defer func() { _ = fd2.Close() }()
|
||||
_ = os.Setenv("AKAIUTIL", "")
|
||||
os.Args[0] = exe
|
||||
got, err := findAkaiutil()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if got != sideBySide {
|
||||
t.Errorf("got %q, want %q", got, sideBySide)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("finds binary in third_party relative to argv[0]", func(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
exe := tmp + "/somebin"
|
||||
fd, _ := os.Create(exe)
|
||||
defer func() { _ = fd.Close() }()
|
||||
thirdParty := filepath.Join(tmp, "third_party", "akaiutil")
|
||||
if err := os.MkdirAll(thirdParty, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
akaiutilInThirdParty := filepath.Join(thirdParty, "akaiutil")
|
||||
fd2, _ := os.Create(akaiutilInThirdParty)
|
||||
defer func() { _ = fd2.Close() }()
|
||||
_ = os.Setenv("AKAIUTIL", "")
|
||||
os.Args[0] = exe
|
||||
got, err := findAkaiutil()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if got != akaiutilInThirdParty {
|
||||
t.Errorf("got %q, want %q", got, akaiutilInThirdParty)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("env var takes priority over path candidates", func(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
envFile := tmp + "/envutil"
|
||||
pathFile := tmp + "/pathutil"
|
||||
fd1, _ := os.Create(envFile)
|
||||
defer func() { _ = fd1.Close() }()
|
||||
fd2, _ := os.Create(pathFile)
|
||||
defer func() { _ = fd2.Close() }()
|
||||
_ = os.Setenv("AKAIUTIL", envFile)
|
||||
os.Args[0] = pathFile
|
||||
got, err := findAkaiutil()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if got != envFile {
|
||||
t.Errorf("got %q, want env var path %q", got, envFile)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no akaiutil found returns error", func(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
exe := tmp + "/nobinaryhere"
|
||||
fd, _ := os.Create(exe)
|
||||
defer func() { _ = fd.Close() }()
|
||||
_ = os.Setenv("AKAIUTIL", "")
|
||||
os.Args[0] = exe
|
||||
_, err := findAkaiutil()
|
||||
if err == nil {
|
||||
t.Fatal("expected error when no akaiutil found")
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user