198 lines
5.7 KiB
Go
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.
|