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
326 lines
8.5 KiB
Go
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)
|
|
}
|
|
}
|
|
} |