feat: initial goget release — modern IPv6-first download utility
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
//go:build linux || freebsd
|
||||
// +build linux freebsd
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestVersion(t *testing.T) {
|
||||
v := Version()
|
||||
if v == "" {
|
||||
t.Error("Version() returned an empty string")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionFormat(t *testing.T) {
|
||||
v := Version()
|
||||
if !strings.Contains(v, ".") {
|
||||
t.Errorf("Version() = %q; expected semver-like format", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewClient(t *testing.T) {
|
||||
client := NewClient()
|
||||
if client == nil {
|
||||
t.Fatal("NewClient() returned nil")
|
||||
}
|
||||
defer client.Close()
|
||||
}
|
||||
|
||||
func TestNewClientWithOptions(t *testing.T) {
|
||||
client := NewClient(WithVerbose(true), WithUserAgent("Test/1.0"))
|
||||
if client == nil {
|
||||
t.Fatal("NewClient() returned nil")
|
||||
}
|
||||
defer client.Close()
|
||||
}
|
||||
|
||||
func TestDownloadRequestValidation(t *testing.T) {
|
||||
client := NewClient()
|
||||
defer client.Close()
|
||||
|
||||
if _, err := client.Download(nil); err == nil {
|
||||
t.Error("expected error for nil request")
|
||||
}
|
||||
if _, err := client.Download(&DownloadRequest{}); err == nil {
|
||||
t.Error("expected error for empty URL")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadRequestValidation(t *testing.T) {
|
||||
client := NewClient()
|
||||
defer client.Close()
|
||||
|
||||
if _, err := client.Upload(nil); err == nil {
|
||||
t.Error("expected error for nil request")
|
||||
}
|
||||
if _, err := client.Upload(&UploadRequest{URL: "http://example.com"}); err == nil {
|
||||
t.Error("expected error for missing FilePath")
|
||||
}
|
||||
if _, err := client.Upload(&UploadRequest{FilePath: "/tmp/test"}); err == nil {
|
||||
t.Error("expected error for missing URL")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloadResultFields(t *testing.T) {
|
||||
result := &DownloadResult{
|
||||
BytesDownloaded: 1024,
|
||||
Protocol: "HTTP/2",
|
||||
}
|
||||
if result.BytesDownloaded != 1024 {
|
||||
t.Errorf("BytesDownloaded = %d", result.BytesDownloaded)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadResultFields(t *testing.T) {
|
||||
result := &UploadResult{
|
||||
BytesUploaded: 512,
|
||||
}
|
||||
if result.BytesUploaded != 512 {
|
||||
t.Errorf("BytesUploaded = %d", result.BytesUploaded)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloadConvenience(t *testing.T) {
|
||||
_, err := Download("", "")
|
||||
if err == nil {
|
||||
t.Error("expected error for empty URL")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadConvenience(t *testing.T) {
|
||||
_, err := Upload("", "", "")
|
||||
if err == nil {
|
||||
t.Error("expected error for empty fields")
|
||||
}
|
||||
}
|
||||
|
||||
// TestRequestContextTimeout is a regression guard for the BACKLOG entry
|
||||
// "`pkg/api` Client timeout is lifetime, not per-request". The previous
|
||||
// implementation created a single context.WithTimeout in NewClient, so a
|
||||
// Client that lived longer than cfg.timeout could no longer serve any
|
||||
// request. The fixed implementation uses a cancellable background context
|
||||
// for the Client scope and derives a per-request child with the timeout
|
||||
// applied at call time.
|
||||
func TestRequestContextTimeout(t *testing.T) {
|
||||
client := NewClient(WithTimeout(5 * time.Second))
|
||||
defer client.Close()
|
||||
|
||||
reqCtx, cancel := client.requestContext()
|
||||
defer cancel()
|
||||
|
||||
deadline, ok := reqCtx.Deadline()
|
||||
if !ok {
|
||||
t.Fatal("per-request context has no deadline; want cfg.timeout")
|
||||
}
|
||||
|
||||
remaining := time.Until(deadline)
|
||||
// Allow generous slack to avoid flakes on a busy CI runner, but
|
||||
// enforce that the deadline is at most the configured timeout and
|
||||
// has not already passed.
|
||||
if remaining <= 0 {
|
||||
t.Errorf("per-request deadline already passed: %v remaining", remaining)
|
||||
}
|
||||
if remaining > 5*time.Second {
|
||||
t.Errorf("per-request deadline = %v from now; want <= 5s (cfg.timeout)", remaining)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRequestContextNoTimeout verifies that a Client constructed with
|
||||
// WithTimeout(0) yields a per-request context without a deadline. This
|
||||
// preserves the "no timeout" semantics that callers relied on before the
|
||||
// per-request refactor, when the only way to disable the lifetime cap
|
||||
// was to never call Download at all.
|
||||
func TestRequestContextNoTimeout(t *testing.T) {
|
||||
client := NewClient(WithTimeout(0))
|
||||
defer client.Close()
|
||||
|
||||
reqCtx, cancel := client.requestContext()
|
||||
defer cancel()
|
||||
|
||||
if _, ok := reqCtx.Deadline(); ok {
|
||||
t.Error("per-request context has a deadline; want none when cfg.timeout == 0")
|
||||
}
|
||||
}
|
||||
|
||||
// TestRequestContextCancelledByClientClose verifies that cancelling the
|
||||
// Client scope propagates to per-request children. A long-running
|
||||
// Download must abort when Close() is called, even if its own deadline
|
||||
// has not been reached.
|
||||
func TestRequestContextCancelledByClientClose(t *testing.T) {
|
||||
client := NewClient(WithTimeout(time.Hour))
|
||||
defer client.Close()
|
||||
|
||||
reqCtx, cancel := client.requestContext()
|
||||
defer cancel()
|
||||
|
||||
// Sanity: nothing is cancelled yet.
|
||||
if err := reqCtx.Err(); err != nil {
|
||||
t.Fatalf("reqCtx unexpectedly cancelled before Close: %v", err)
|
||||
}
|
||||
|
||||
client.Close()
|
||||
|
||||
// Close cancels the client scope, which propagates to the child.
|
||||
select {
|
||||
case <-reqCtx.Done():
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("per-request context not cancelled after Client.Close()")
|
||||
}
|
||||
if err := reqCtx.Err(); err == nil {
|
||||
t.Error("reqCtx.Err() = nil after Client.Close(); want non-nil")
|
||||
}
|
||||
}
|
||||
|
||||
// TestClientScopeHasNoDeadline is a regression guard for the original
|
||||
// bug. The previous implementation wrapped context.Background() in
|
||||
// WithTimeout at NewClient time, so a Client that lived past cfg.timeout
|
||||
// had a context that was already past its deadline — and any
|
||||
// context.WithTimeout child would be expired immediately.
|
||||
func TestClientScopeHasNoDeadline(t *testing.T) {
|
||||
client := NewClient(WithTimeout(50 * time.Millisecond))
|
||||
defer client.Close()
|
||||
|
||||
if _, ok := client.ctx.Deadline(); ok {
|
||||
t.Error("client-scope context has a deadline; lifetime cap would re-introduce the bug")
|
||||
}
|
||||
}
|
||||
|
||||
// TestRequestContextCancelledByClientClose above already proves that
|
||||
// per-request contexts are derived from the client scope: cancelling the
|
||||
// client cancels the per-request child. A direct parent-chain walk is not
|
||||
// possible from outside the context package because the `parent` field
|
||||
// is unexported.
|
||||
Reference in New Issue
Block a user