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