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