Files
goget/pkg/api/api_test.go
T

198 lines
5.7 KiB
Go

//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.