Files
akai-utils/download_test.go
T
david 120993fb02 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
2026-06-21 23:54:57 -07:00

326 lines
8.5 KiB
Go

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)
}
}
}