starnet/addon_test.go
2026-03-08 20:19:40 +08:00

1658 lines
43 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package starnet
import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
)
// TestComplexScenario1_RequestLevelConfigOverride 测试请求级配置覆盖 Client 级配置
func TestComplexScenario1_RequestLevelConfigOverride(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(150 * time.Millisecond)
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}))
defer server.Close()
// Client 级别5 秒超时
client := NewClientNoErr(WithTimeout(5 * time.Second))
// 请求 1使用 Client 的超时(应该成功)
resp1, err := client.Get(server.URL)
if err != nil {
t.Fatalf("Request 1 error: %v", err)
}
resp1.Close()
// 请求 2请求级别覆盖为 100ms应该超时
start := time.Now()
_, err = client.Get(server.URL, WithTimeout(100*time.Millisecond))
elapsed := time.Since(start)
if err == nil {
t.Error("Request 2 should timeout, got nil error")
}
if elapsed > 500*time.Millisecond {
t.Errorf("Request 2 timeout took too long: %v", elapsed)
}
// 请求 3再次使用 Client 的超时(应该成功,验证没有副作用)
resp3, err := client.Get(server.URL)
if err != nil {
t.Fatalf("Request 3 error: %v", err)
}
resp3.Close()
}
// TestComplexScenario2_TLSConfigPriority 测试 TLS 配置的优先级
func TestComplexScenario2_TLSConfigPriority(t *testing.T) {
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}))
defer server.Close()
// 场景 1Client 级别设置 SkipVerify
client := NewClientNoErr()
client.SetDefaultSkipTLSVerify(true)
resp1, err := client.Get(server.URL)
if err != nil {
t.Fatalf("Scenario 1 error: %v", err)
}
resp1.Close()
// 场景 2请求级别设置自定义 TLS Config应该覆盖 Client 级别)
customTLS := &tls.Config{
InsecureSkipVerify: true,
MinVersion: tls.VersionTLS12,
}
resp2, err := client.Get(server.URL, WithTLSConfig(customTLS))
if err != nil {
t.Fatalf("Scenario 2 error: %v", err)
}
resp2.Close()
// 场景 3请求级别只设置 SkipVerify不设置完整 TLS Config
resp3, err := client.Get(server.URL, WithSkipTLSVerify(true))
if err != nil {
t.Fatalf("Scenario 3 error: %v", err)
}
resp3.Close()
// 场景 4新 Client 不设置任何 TLS 配置(应该失败)
client2 := NewClientNoErr()
_, err = client2.Get(server.URL)
if err == nil {
t.Error("Scenario 4 should fail with TLS error, got nil")
}
}
// TestComplexScenario3_ConnectionPoolReuse 测试连接池复用
func TestComplexScenario3_ConnectionPoolReuse(t *testing.T) {
var connCount int64
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt64(&connCount, 1)
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}))
defer server.Close()
client := NewClientNoErr()
// 发送 10 个请求,应该复用连接
for i := 0; i < 10; i++ {
resp, err := client.Get(server.URL)
if err != nil {
t.Fatalf("Request %d error: %v", i, err)
}
// 必须读取并关闭 body 才能复用连接
io.ReadAll(resp.Body().raw)
resp.Close()
}
// 验证连接被复用(实际连接数应该远小于请求数)
// 注意:这个测试可能不稳定,因为连接池行为依赖于时间和系统状态
t.Logf("Total handler calls: %d", atomic.LoadInt64(&connCount))
}
// TestComplexScenario4_CustomDNSWithFallback 测试自定义 DNS 和回退机制
func TestComplexScenario4_CustomDNSWithFallback(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}))
defer server.Close()
// 提取服务器的实际 IP 和端口
serverURL := server.URL
host := strings.TrimPrefix(serverURL, "http://")
// 场景 1使用自定义 IP直接指定
parts := strings.Split(host, ":")
if len(parts) != 2 {
t.Fatalf("Invalid server URL: %s", serverURL)
}
ip := parts[0]
port := parts[1]
// 构造一个使用域名的 URL
testURL := fmt.Sprintf("http://test.example.com:%s", port)
req := NewSimpleRequest(testURL, "GET").SetCustomIP([]string{ip})
resp, err := req.Do()
if err != nil {
t.Fatalf("Custom IP request error: %v", err)
}
resp.Close()
// 场景 2使用自定义 DNS 解析函数
lookupCalled := false
customLookup := func(ctx context.Context, host string) ([]net.IPAddr, error) {
lookupCalled = true
// 返回实际的 IP
return []net.IPAddr{{IP: net.ParseIP(ip)}}, nil
}
req2 := NewSimpleRequest(testURL, "GET").SetLookupFunc(customLookup)
resp2, err := req2.Do()
if err != nil {
t.Fatalf("Custom lookup request error: %v", err)
}
resp2.Close()
if !lookupCalled {
t.Error("Custom lookup function was not called")
}
}
// TestComplexScenario5_ConcurrentRequestsWithDifferentConfigs 测试并发请求使用不同配置
func TestComplexScenario5_ConcurrentRequestsWithDifferentConfigs(t *testing.T) {
// 创建多个服务器,模拟不同的延迟
servers := make([]*httptest.Server, 3)
for i := range servers {
delay := time.Duration(i*50) * time.Millisecond
idx := i // ← 修复:创建局部变量
servers[i] = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(delay)
w.WriteHeader(http.StatusOK)
w.Write([]byte(fmt.Sprintf("Server %d", idx))) // ← 使用局部变量
}))
defer servers[i].Close()
}
client := NewClientNoErr()
var wg sync.WaitGroup
results := make([]string, 3)
errors := make([]error, 3)
// 并发发送请求,每个请求使用不同的超时
for i := 0; i < 3; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
timeout := time.Duration((idx+1)*100) * time.Millisecond
resp, err := client.Get(servers[idx].URL, WithTimeout(timeout))
if err != nil {
errors[idx] = err
return
}
defer resp.Close()
body, _ := resp.Body().String()
results[idx] = body
}(i)
}
wg.Wait()
// 验证结果
for i := 0; i < 3; i++ {
if errors[i] != nil {
t.Errorf("Request %d error: %v", i, errors[i])
}
expected := fmt.Sprintf("Server %d", i)
if results[i] != expected {
t.Errorf("Request %d result = %v; want %v", i, results[i], expected)
}
}
}
// TestComplexScenario6_RequestCloneIndependence 测试克隆请求的独立性
func TestComplexScenario6_RequestCloneIndependence(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 返回所有 headers
for k, v := range r.Header {
w.Header().Set(k, strings.Join(v, ","))
}
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
// 创建基础请求
baseReq := NewSimpleRequest(server.URL, "GET").
SetHeader("X-Base", "base-value").
SetTimeout(5 * time.Second)
// 克隆并修改
req1 := baseReq.Clone().
SetHeader("X-Request", "request-1").
SetTimeout(1 * time.Second)
req2 := baseReq.Clone().
SetHeader("X-Request", "request-2").
SetTimeout(2 * time.Second)
// 执行请求
resp1, err := req1.Do()
if err != nil {
t.Fatalf("Request 1 error: %v", err)
}
defer resp1.Close()
resp2, err := req2.Do()
if err != nil {
t.Fatalf("Request 2 error: %v", err)
}
defer resp2.Close()
// 验证 headers 独立
if resp1.Header.Get("X-Request") != "request-1" {
t.Errorf("Request 1 header = %v; want request-1", resp1.Header.Get("X-Request"))
}
if resp2.Header.Get("X-Request") != "request-2" {
t.Errorf("Request 2 header = %v; want request-2", resp2.Header.Get("X-Request"))
}
// 验证基础请求未被修改
resp3, err := baseReq.Do()
if err != nil {
t.Fatalf("Base request error: %v", err)
}
defer resp3.Close()
if resp3.Header.Get("X-Request") != "" {
t.Errorf("Base request should not have X-Request header, got %v", resp3.Header.Get("X-Request"))
}
}
// TestComplexScenario7_ErrorAccumulation 测试错误累积机制
func TestComplexScenario7_ErrorAccumulation(t *testing.T) {
// 场景 1链式调用中的错误累积
req := NewSimpleRequest("://invalid-url", "GET").
SetHeader("X-Test", "value").
AddQuery("key", "value")
// 错误应该被累积,不会 panic
if req.Err() == nil {
t.Error("Expected error for invalid URL, got nil")
}
// 后续操作应该被忽略
req.SetTimeout(5 * time.Second)
// Do() 应该返回累积的错误
_, err := req.Do()
if err == nil {
t.Error("Do() should return accumulated error, got nil")
}
// 场景 2无效的方法
req2 := NewSimpleRequest("http://example.com", "INVALID METHOD!")
if req2.Err() == nil {
t.Error("Expected error for invalid method, got nil")
}
// 场景 3无效的 IP
req3 := NewSimpleRequest("http://example.com", "GET").
SetCustomIP([]string{"invalid-ip"})
if req3.Err() == nil {
t.Error("Expected error for invalid IP, got nil")
}
}
// TestComplexScenario8_DialTimeoutVsRequestTimeout 测试 DialTimeout 和 Timeout 的区别
func TestComplexScenario8_DialTimeoutVsRequestTimeout(t *testing.T) {
// 场景 1DialTimeout - 连接超时
start := time.Now()
req := NewSimpleRequest("http://192.0.2.1:80", "GET").
SetDialTimeout(100 * time.Millisecond)
_, err := req.Do()
elapsed := time.Since(start)
if err == nil {
t.Error("Expected dial timeout error, got nil")
}
if elapsed > 2*time.Second {
t.Errorf("Dial timeout took too long: %v", elapsed)
}
// 场景 2Timeout - 总超时(包括响应读取)
slowServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(200 * time.Millisecond)
w.WriteHeader(http.StatusOK)
}))
defer slowServer.Close()
start2 := time.Now()
req2 := NewSimpleRequest(slowServer.URL, "GET").
SetTimeout(100 * time.Millisecond)
_, err2 := req2.Do()
elapsed2 := time.Since(start2)
if err2 == nil {
t.Error("Expected request timeout error, got nil")
}
if elapsed2 > 500*time.Millisecond {
t.Errorf("Request timeout took too long: %v", elapsed2)
}
}
// TestComplexScenario9_MultipartUploadWithProgress 测试带进度的文件上传
func TestComplexScenario9_MultipartUploadWithProgress(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
err := r.ParseMultipartForm(10 << 20)
if err != nil {
t.Errorf("ParseMultipartForm error: %v", err)
w.WriteHeader(http.StatusBadRequest)
return
}
// 验证表单字段
if r.FormValue("name") != "test" {
t.Errorf("name = %v; want test", r.FormValue("name"))
}
// 验证文件
file, header, err := r.FormFile("file")
if err != nil {
t.Errorf("FormFile error: %v", err)
w.WriteHeader(http.StatusBadRequest)
return
}
defer file.Close()
content, _ := io.ReadAll(file)
w.WriteHeader(http.StatusOK)
w.Write([]byte(fmt.Sprintf("Received: %s (%d bytes)", header.Filename, len(content))))
}))
defer server.Close()
// 创建测试数据
fileContent := strings.Repeat("test data ", 1000) // ~10KB
reader := strings.NewReader(fileContent)
// 跟踪进度
var progressCalls int64
var lastUploaded int64
req := NewSimpleRequest(server.URL, "POST").
AddFormData("name", "test").
AddFileStream("file", "test.txt", int64(len(fileContent)), reader).
SetUploadProgress(func(filename string, uploaded, total int64) {
atomic.AddInt64(&progressCalls, 1)
atomic.StoreInt64(&lastUploaded, uploaded)
if filename != "test.txt" {
t.Errorf("filename = %v; want test.txt", filename)
}
if total != int64(len(fileContent)) {
t.Errorf("total = %v; want %v", total, len(fileContent))
}
})
resp, err := req.Do()
if err != nil {
t.Fatalf("Upload error: %v", err)
}
defer resp.Close()
// 验证进度回调被调用
if atomic.LoadInt64(&progressCalls) == 0 {
t.Error("Progress callback was not called")
}
// 验证最终上传量
if atomic.LoadInt64(&lastUploaded) != int64(len(fileContent)) {
t.Errorf("lastUploaded = %v; want %v", lastUploaded, len(fileContent))
}
body, _ := resp.Body().String()
if !strings.Contains(body, "test.txt") {
t.Errorf("Response should contain filename, got: %v", body)
}
}
// TestComplexScenario10_ClientCloneWithOptions 测试 Client 克隆和选项继承
func TestComplexScenario10_ClientCloneWithOptions(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(r.Header.Get("X-Client-ID")))
}))
defer server.Close()
// 创建带选项的 Client
client1 := NewClientNoErr(
WithTimeout(5*time.Second),
WithHeader("X-Client-ID", "client-1"),
)
// 克隆 Client
client2 := client1.Clone()
client2.AddOptions(WithHeader("X-Extra", "extra-value"))
// 测试 client1
resp1, err := client1.Get(server.URL)
if err != nil {
t.Fatalf("Client 1 error: %v", err)
}
defer resp1.Close()
body1, _ := resp1.Body().String()
if body1 != "client-1" {
t.Errorf("Client 1 response = %v; want client-1", body1)
}
// 测试 client2应该继承 client1 的选项)
resp2, err := client2.Get(server.URL)
if err != nil {
t.Fatalf("Client 2 error: %v", err)
}
defer resp2.Close()
body2, _ := resp2.Body().String()
if body2 != "client-1" {
t.Errorf("Client 2 response = %v; want client-1", body2)
}
// 验证 client1 未被修改
opts1 := client1.RequestOptions()
opts2 := client2.RequestOptions()
if len(opts1) >= len(opts2) {
t.Errorf("Client 2 should have more options than Client 1")
}
}
// TestComplexScenario11_ContextCancellation 测试 Context 取消
func TestComplexScenario11_ContextCancellation(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(2 * time.Second)
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
ctx, cancel := context.WithCancel(context.Background())
// 在 500ms 后取消
go func() {
time.Sleep(500 * time.Millisecond)
cancel()
}()
req := NewSimpleRequestWithContext(ctx, server.URL, "GET")
start := time.Now()
_, err := req.Do()
elapsed := time.Since(start)
if err == nil {
t.Error("Expected context cancellation error, got nil")
}
if elapsed > 1*time.Second {
t.Errorf("Context cancellation took too long: %v", elapsed)
}
}
// TestComplexScenario12_RedirectWithCookies 测试重定向时的 Cookie 处理
func TestComplexScenario12_RedirectWithCookies(t *testing.T) {
var redirectCount int
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if redirectCount < 2 {
redirectCount++
// 设置 Cookie 并重定向
http.SetCookie(w, &http.Cookie{
Name: fmt.Sprintf("cookie%d", redirectCount),
Value: fmt.Sprintf("value%d", redirectCount),
Path: "/",
})
http.Redirect(w, r, "/final", http.StatusFound)
return
}
// 最终响应
w.WriteHeader(http.StatusOK)
w.Write([]byte("final"))
}))
defer server.Close()
// 测试自动跟随重定向
client := NewClientNoErr()
resp, err := client.Get(server.URL)
if err != nil {
t.Fatalf("Get error: %v", err)
}
defer resp.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("StatusCode = %v; want %v", resp.StatusCode, http.StatusOK)
}
body, _ := resp.Body().String()
if body != "final" {
t.Errorf("Body = %v; want final", body)
}
// 测试禁用重定向
redirectCount = 0
client.DisableRedirect()
resp2, err := client.Get(server.URL)
if err != nil {
t.Fatalf("Get error: %v", err)
}
defer resp2.Close()
if resp2.StatusCode != http.StatusFound {
t.Errorf("StatusCode = %v; want %v", resp2.StatusCode, http.StatusFound)
}
// 验证 Set-Cookie
cookies := resp2.Cookies()
if len(cookies) == 0 {
t.Error("Expected cookies in redirect response")
}
}
// TestDefaultsSetDefaultClient 测试设置默认 Client
func TestDefaultsSetDefaultClient(t *testing.T) {
// 保存原始的默认 Client
originalClient := DefaultClient()
// 创建自定义 Client
customClient := NewClientNoErr(WithTimeout(1 * time.Second))
SetDefaultClient(customClient)
// 验证默认 Client 已更改
if DefaultClient() != customClient {
t.Error("SetDefaultClient did not update default client")
}
// 恢复原始 Client
SetDefaultClient(originalClient)
}
// TestDefaultsSetDefaultHTTPClient 测试设置默认 HTTP Client
func TestDefaultsSetDefaultHTTPClient(t *testing.T) {
// 保存原始的默认 HTTP Client
originalHTTPClient := DefaultHTTPClient()
// 创建自定义 HTTP Client
customHTTPClient := &http.Client{
Timeout: 2 * time.Second,
}
SetDefaultHTTPClient(customHTTPClient)
// 验证默认 HTTP Client 已更改
if DefaultHTTPClient() != customHTTPClient {
t.Error("SetDefaultHTTPClient did not update default http client")
}
// 恢复原始 HTTP Client
SetDefaultHTTPClient(originalHTTPClient)
}
// TestDefaultsHeadMethod 测试 Head 方法
func TestDefaultsHeadMethod(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodHead {
t.Errorf("Method = %v; want HEAD", r.Method)
}
w.Header().Set("X-Custom", "test-value")
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
resp, err := Head(server.URL)
if err != nil {
t.Fatalf("Head() error: %v", err)
}
defer resp.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("StatusCode = %v; want %v", resp.StatusCode, http.StatusOK)
}
// HEAD 请求应该有 headers 但没有 body
if resp.Header.Get("X-Custom") != "test-value" {
t.Errorf("Header X-Custom = %v; want test-value", resp.Header.Get("X-Custom"))
}
}
// TestProxyConfiguration 测试代理配置
func TestProxyConfiguration(t *testing.T) {
// 创建目标服务器
targetServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("target"))
}))
defer targetServer.Close()
// 创建代理服务器
proxyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 简单的代理逻辑
w.Header().Set("X-Proxied", "true")
w.WriteHeader(http.StatusOK)
w.Write([]byte("proxied"))
}))
defer proxyServer.Close()
// 测试 WithProxy
req := NewSimpleRequest(targetServer.URL, "GET").SetProxy(proxyServer.URL)
// 验证代理配置被设置
if req.config.Network.Proxy != proxyServer.URL {
t.Errorf("Proxy = %v; want %v", req.config.Network.Proxy, proxyServer.URL)
}
// 注意:实际的代理测试需要真实的代理服务器
// 这里只验证配置是否正确设置
}
// TestWithRawRequest 测试 WithRawRequest
func TestWithRawRequest(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("X-Custom") != "raw-value" {
t.Errorf("X-Custom header = %v; want raw-value", r.Header.Get("X-Custom"))
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}))
defer server.Close()
// 创建原始 http.Request
rawReq, _ := http.NewRequest("GET", server.URL, nil)
rawReq.Header.Set("X-Custom", "raw-value")
// 使用 WithRawRequest
req := NewSimpleRequest("", "GET", WithRawRequest(rawReq))
resp, err := req.Do()
if err != nil {
t.Fatalf("Do() error: %v", err)
}
defer resp.Close()
body, _ := resp.Body().String()
if body != "OK" {
t.Errorf("Body = %v; want OK", body)
}
}
// TestWithContentLength 测试 WithContentLength
func TestWithContentLength(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.ContentLength != 9 {
t.Errorf("ContentLength = %v; want 9", r.ContentLength)
}
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
data := []byte("test data")
resp, err := Post(server.URL,
WithBody(data),
WithContentLength(int64(len(data)))) // 一致
if err != nil {
t.Fatalf("Post() error: %v", err)
}
defer resp.Close()
}
// TestWithAutoCalcContentLength 测试自动计算 Content-Length
func TestWithAutoCalcContentLength(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 验证 Content-Length 被正确设置
if r.ContentLength <= 0 {
t.Errorf("ContentLength = %v; want > 0", r.ContentLength)
}
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
data := strings.NewReader("test data for auto calc")
resp, err := Post(server.URL,
WithBodyReader(data),
WithAutoCalcContentLength(true))
if err != nil {
t.Fatalf("Post() error: %v", err)
}
defer resp.Close()
}
// TestChunkedTransferEncoding 测试 Chunked 传输编码
func TestChunkedTransferEncoding(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 验证使用了 chunked 编码
if len(r.TransferEncoding) > 0 && r.TransferEncoding[0] == "chunked" {
w.Header().Set("X-Chunked", "true")
}
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
data := []byte("test data")
resp, err := Post(server.URL,
WithBody(data),
WithContentLength(-1)) // -1 强制使用 chunked
if err != nil {
t.Fatalf("Post() error: %v", err)
}
defer resp.Close()
}
// TestWithFormDataMap 测试 WithFormDataMap
func TestWithFormDataMap(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
if r.FormValue("key1") != "value1" {
t.Errorf("key1 = %v; want value1", r.FormValue("key1"))
}
if r.FormValue("key2") != "value2" {
t.Errorf("key2 = %v; want value2", r.FormValue("key2"))
}
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
resp, err := Post(server.URL,
WithFormDataMap(map[string]string{
"key1": "value1",
"key2": "value2",
}))
if err != nil {
t.Fatalf("Post() error: %v", err)
}
defer resp.Close()
}
// TestWithFormData 测试 WithFormData
func TestWithFormData(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
values := r.Form["tags"]
if len(values) != 2 {
t.Errorf("tags length = %v; want 2", len(values))
}
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
resp, err := Post(server.URL,
WithFormData(map[string][]string{
"tags": {"tag1", "tag2"},
}))
if err != nil {
t.Fatalf("Post() error: %v", err)
}
defer resp.Close()
}
// TestWithAddFormData 测试 WithAddFormData
func TestWithAddFormData(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
if r.FormValue("name") != "test" {
t.Errorf("name = %v; want test", r.FormValue("name"))
}
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
resp, err := Post(server.URL,
WithAddFormData("name", "test"))
if err != nil {
t.Fatalf("Post() error: %v", err)
}
defer resp.Close()
}
// TestHeaderOperations 测试 Header 操作
func TestHeaderOperations(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
headers := make(map[string][]string)
for k, v := range r.Header {
headers[k] = v
}
json.NewEncoder(w).Encode(headers)
}))
defer server.Close()
req := NewSimpleRequest(server.URL, "GET")
// AddHeader
req.AddHeader("X-Multi", "value1")
req.AddHeader("X-Multi", "value2")
// SetHeader
req.SetHeader("X-Single", "single-value")
// DeleteHeader
req.SetHeader("X-Delete", "will-be-deleted")
req.DeleteHeader("X-Delete")
// ResetHeaders
req2 := NewSimpleRequest(server.URL, "GET")
req2.SetHeader("X-Test", "test")
req2.ResetHeaders()
req2.SetHeader("X-After-Reset", "value")
resp, err := req.Do()
if err != nil {
t.Fatalf("Do() error: %v", err)
}
defer resp.Close()
var headers map[string][]string
resp.Body().JSON(&headers)
// 验证 AddHeader
if len(headers["X-Multi"]) != 2 {
t.Errorf("X-Multi length = %v; want 2", len(headers["X-Multi"]))
}
// 验证 SetHeader
if headers["X-Single"][0] != "single-value" {
t.Errorf("X-Single = %v; want single-value", headers["X-Single"][0])
}
// 验证 DeleteHeader
if _, exists := headers["X-Delete"]; exists {
t.Error("X-Delete should be deleted")
}
// 测试 ResetHeaders
resp2, err := req2.Do()
if err != nil {
t.Fatalf("Do() error: %v", err)
}
defer resp2.Close()
var headers2 map[string][]string
resp2.Body().JSON(&headers2)
if _, exists := headers2["X-Test"]; exists {
t.Error("X-Test should not exist after reset")
}
if headers2["X-After-Reset"][0] != "value" {
t.Errorf("X-After-Reset = %v; want value", headers2["X-After-Reset"][0])
}
}
// TestCookieOperations 测试 Cookie 操作
func TestCookieOperations(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
cookies := make(map[string]string)
for _, cookie := range r.Cookies() {
cookies[cookie.Name] = cookie.Value
}
json.NewEncoder(w).Encode(cookies)
}))
defer server.Close()
req := NewSimpleRequest(server.URL, "GET")
// AddSimpleCookie
req.AddSimpleCookie("simple", "simple-value")
// AddCookieKV
req.AddCookieKV("custom", "custom-value", "/path")
// AddCookie
req.AddCookie(&http.Cookie{
Name: "full",
Value: "full-value",
Path: "/",
})
resp, err := req.Do()
if err != nil {
t.Fatalf("Do() error: %v", err)
}
defer resp.Close()
var cookies map[string]string
resp.Body().JSON(&cookies)
if cookies["simple"] != "simple-value" {
t.Errorf("simple = %v; want simple-value", cookies["simple"])
}
if cookies["custom"] != "custom-value" {
t.Errorf("custom = %v; want custom-value", cookies["custom"])
}
if cookies["full"] != "full-value" {
t.Errorf("full = %v; want full-value", cookies["full"])
}
// 测试 ResetCookies
req2 := NewSimpleRequest(server.URL, "GET")
req2.AddSimpleCookie("before", "before-value")
req2.ResetCookies()
req2.AddSimpleCookie("after", "after-value")
resp2, err := req2.Do()
if err != nil {
t.Fatalf("Do() error: %v", err)
}
defer resp2.Close()
var cookies2 map[string]string
resp2.Body().JSON(&cookies2)
if _, exists := cookies2["before"]; exists {
t.Error("before cookie should not exist after reset")
}
if cookies2["after"] != "after-value" {
t.Errorf("after = %v; want after-value", cookies2["after"])
}
}
// TestQueryOperations 测试 Query 操作
func TestQueryOperations(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
result := make(map[string][]string)
for k, v := range query {
result[k] = v
}
json.NewEncoder(w).Encode(result)
}))
defer server.Close()
req := NewSimpleRequest(server.URL, "GET")
// AddQuery
req.AddQuery("multi", "value1")
req.AddQuery("multi", "value2")
// SetQuery
req.SetQuery("single", "single-value")
// AddQueries
req.AddQueries(map[string]string{
"batch1": "batch-value1",
"batch2": "batch-value2",
})
// DeleteQuery
req.AddQuery("delete-me", "will-be-deleted")
req.DeleteQuery("delete-me")
// DeleteQueryValue
req.AddQuery("partial", "keep")
req.AddQuery("partial", "delete")
req.DeleteQueryValue("partial", "delete")
resp, err := req.Do()
if err != nil {
t.Fatalf("Do() error: %v", err)
}
defer resp.Close()
var result map[string][]string
resp.Body().JSON(&result)
// 验证 AddQuery
if len(result["multi"]) != 2 {
t.Errorf("multi length = %v; want 2", len(result["multi"]))
}
// 验证 SetQuery
if len(result["single"]) != 1 || result["single"][0] != "single-value" {
t.Errorf("single = %v; want [single-value]", result["single"])
}
// 验证 AddQueries
if result["batch1"][0] != "batch-value1" {
t.Errorf("batch1 = %v; want batch-value1", result["batch1"][0])
}
// 验证 DeleteQuery
if _, exists := result["delete-me"]; exists {
t.Error("delete-me should not exist")
}
// 验证 DeleteQueryValue
if len(result["partial"]) != 1 || result["partial"][0] != "keep" {
t.Errorf("partial = %v; want [keep]", result["partial"])
}
}
// TestWithCookies 测试 WithCookies
func TestWithCookies(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
cookies := make(map[string]string)
for _, cookie := range r.Cookies() {
cookies[cookie.Name] = cookie.Value
}
json.NewEncoder(w).Encode(cookies)
}))
defer server.Close()
resp, err := Get(server.URL,
WithCookies(map[string]string{
"cookie1": "value1",
"cookie2": "value2",
}))
if err != nil {
t.Fatalf("Get() error: %v", err)
}
defer resp.Close()
var cookies map[string]string
resp.Body().JSON(&cookies)
if cookies["cookie1"] != "value1" {
t.Errorf("cookie1 = %v; want value1", cookies["cookie1"])
}
if cookies["cookie2"] != "value2" {
t.Errorf("cookie2 = %v; want value2", cookies["cookie2"])
}
}
// TestWithHeaders 测试 WithHeaders
func TestWithHeaders(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("X-Header1") != "value1" {
t.Errorf("X-Header1 = %v; want value1", r.Header.Get("X-Header1"))
}
if r.Header.Get("X-Header2") != "value2" {
t.Errorf("X-Header2 = %v; want value2", r.Header.Get("X-Header2"))
}
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
resp, err := Get(server.URL,
WithHeaders(map[string]string{
"X-Header1": "value1",
"X-Header2": "value2",
}))
if err != nil {
t.Fatalf("Get() error: %v", err)
}
defer resp.Close()
}
// TestWithQueries 测试 WithQueries
func TestWithQueries(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
json.NewEncoder(w).Encode(query)
}))
defer server.Close()
resp, err := Get(server.URL,
WithQueries(map[string]string{
"key1": "value1",
"key2": "value2",
}))
if err != nil {
t.Fatalf("Get() error: %v", err)
}
defer resp.Close()
var result map[string][]string
resp.Body().JSON(&result)
if result["key1"][0] != "value1" {
t.Errorf("key1 = %v; want value1", result["key1"][0])
}
if result["key2"][0] != "value2" {
t.Errorf("key2 = %v; want value2", result["key2"][0])
}
}
// TestSetReferer 测试 SetReferer
func TestSetReferer(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Referer() != "https://example.com" {
t.Errorf("Referer = %v; want https://example.com", r.Referer())
}
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
req := NewSimpleRequest(server.URL, "GET").
SetReferer("https://example.com")
resp, err := req.Do()
if err != nil {
t.Fatalf("Do() error: %v", err)
}
defer resp.Close()
}
// TestSetBearerToken 测试 SetBearerToken
func TestSetBearerToken(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
auth := r.Header.Get("Authorization")
if auth != "Bearer test-token-123" {
t.Errorf("Authorization = %v; want Bearer test-token-123", auth)
}
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
req := NewSimpleRequest(server.URL, "GET").
SetBearerToken("test-token-123")
resp, err := req.Do()
if err != nil {
t.Fatalf("Do() error: %v", err)
}
defer resp.Close()
}
// TestGetHeader 测试 GetHeader
func TestGetHeader(t *testing.T) {
req := NewSimpleRequest("http://example.com", "GET")
req.SetHeader("X-Test", "test-value")
value := req.GetHeader("X-Test")
if value != "test-value" {
t.Errorf("GetHeader = %v; want test-value", value)
}
}
// TestEnableDisableRawMode 测试 EnableRawMode 和 DisableRawMode
func TestEnableDisableRawMode(t *testing.T) {
req := NewSimpleRequest("http://example.com", "GET")
// 默认不是 raw 模式
if req.doRaw {
t.Error("Request should not be in raw mode by default")
}
// 启用 raw 模式
req.EnableRawMode()
if !req.doRaw {
t.Error("EnableRawMode should enable raw mode")
}
// 禁用 raw 模式
req.DisableRawMode()
if req.doRaw {
t.Error("DisableRawMode should disable raw mode")
}
}
// TestContextOperations 测试 Context 操作
func TestContextOperations(t *testing.T) {
ctx := context.WithValue(context.Background(), "test-key", "test-value")
req := NewSimpleRequest("http://example.com", "GET")
req.SetContext(ctx)
if req.Context() != ctx {
t.Error("SetContext did not set context correctly")
}
// 验证 context 中的值
if req.Context().Value("test-key") != "test-value" {
t.Error("Context value not preserved")
}
}
// TestRawRequestOperations 测试 RawRequest 操作
func TestRawRequestOperations(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
rawReq, _ := http.NewRequest("GET", server.URL, nil)
rawReq.Header.Set("X-Raw", "raw-value")
req := NewSimpleRequest("", "GET")
req.SetRawRequest(rawReq)
if req.RawRequest() != rawReq {
t.Error("SetRawRequest did not set raw request correctly")
}
resp, err := req.Do()
if err != nil {
t.Fatalf("Do() error: %v", err)
}
defer resp.Close()
}
// TestURLOperations 测试 URL 操作
func TestURLOperations(t *testing.T) {
req := NewSimpleRequest("http://example.com", "GET")
if req.URL() != "http://example.com" {
t.Errorf("URL() = %v; want http://example.com", req.URL())
}
req.SetURL("http://newexample.com")
if req.URL() != "http://newexample.com" {
t.Errorf("URL() after SetURL = %v; want http://newexample.com", req.URL())
}
}
// TestMethodOperations 测试 Method 操作
func TestMethodOperations(t *testing.T) {
req := NewSimpleRequest("http://example.com", "GET")
if req.Method() != "GET" {
t.Errorf("Method() = %v; want GET", req.Method())
}
req.SetMethod("POST")
if req.Method() != "POST" {
t.Errorf("Method() after SetMethod = %v; want POST", req.Method())
}
}
// ---- Client: SetDefaultTLSConfig / EnableRedirect / Options / NewClientFromHTTP ----
func TestClientSetDefaultTLSConfig(t *testing.T) {
ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
defer ts.Close()
c := NewClientNoErr()
c.SetDefaultTLSConfig(&tls.Config{InsecureSkipVerify: true})
resp, err := c.Get(ts.URL)
if err != nil {
t.Fatalf("Get() error: %v", err)
}
defer resp.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("StatusCode=%d", resp.StatusCode)
}
}
func TestClientEnableRedirect(t *testing.T) {
n := 0
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if n == 0 {
n++
http.Redirect(w, r, "/ok", http.StatusFound)
return
}
w.WriteHeader(http.StatusOK)
}))
defer s.Close()
c := NewClientNoErr()
c.DisableRedirect()
resp, err := c.Get(s.URL)
if err != nil {
t.Fatalf("Get() error: %v", err)
}
resp.Close()
if resp.StatusCode != http.StatusFound {
t.Fatalf("want 302, got %d", resp.StatusCode)
}
c.EnableRedirect()
resp2, err := c.Get(s.URL)
if err != nil {
t.Fatalf("Get() after EnableRedirect error: %v", err)
}
defer resp2.Close()
if resp2.StatusCode != http.StatusOK {
t.Fatalf("want 200, got %d", resp2.StatusCode)
}
}
func TestClientOptionsMethod(t *testing.T) {
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodOptions {
t.Fatalf("method=%s", r.Method)
}
w.WriteHeader(http.StatusNoContent)
}))
defer s.Close()
c := NewClientNoErr()
resp, err := c.Options(s.URL)
if err != nil {
t.Fatalf("Options() error: %v", err)
}
defer resp.Close()
if resp.StatusCode != http.StatusNoContent {
t.Fatalf("status=%d", resp.StatusCode)
}
}
func TestNewClientFromHTTP_WithConfiguredTransport(t *testing.T) {
hc := &http.Client{
Transport: &http.Transport{
MaxIdleConns: 17,
},
Timeout: 3 * time.Second,
}
c, err := NewClientFromHTTP(hc)
if err != nil {
t.Fatalf("NewClientFromHTTP error: %v", err)
}
if c == nil || c.HTTPClient() == nil {
t.Fatal("client nil")
}
// 覆盖“http.Client 已有 *http.Transport 的包装路径”
if _, ok := c.HTTPClient().Transport.(*Transport); !ok {
t.Fatalf("transport not wrapped to *Transport, got %T", c.HTTPClient().Transport)
}
}
// ---- context / getRequestContext 覆盖缺口 ----
func TestGetRequestContext_AllMissingBranches(t *testing.T) {
dialFn := func(ctx context.Context, network, addr string) (net.Conn, error) { return nil, nil }
tr := &http.Transport{}
ctx := context.Background()
ctx = context.WithValue(ctx, ctxKeyTransport, tr)
ctx = context.WithValue(ctx, ctxKeyProxy, "http://127.0.0.1:29992")
ctx = context.WithValue(ctx, ctxKeyCustomDNS, []string{"8.8.8.8"})
ctx = context.WithValue(ctx, ctxKeyDialFunc, dialFn)
rc := getRequestContext(ctx)
if rc.Transport != tr {
t.Fatal("transport not extracted")
}
if rc.Proxy != "http://127.0.0.1:29992" {
t.Fatal("proxy not extracted")
}
if len(rc.CustomDNS) != 1 || rc.CustomDNS[0] != "8.8.8.8" {
t.Fatal("custom dns not extracted")
}
if rc.DialFn == nil {
t.Fatal("dialFn not extracted")
}
}
// ---- 默认函数: put/delete/patch/options/trace/connect ----
func TestDefaultMethodsCoverage(t *testing.T) {
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(r.Method))
}))
defer s.Close()
cases := []struct {
name string
fn func(string, ...RequestOpt) (*Response, error)
want string
}{
{"PUT", Put, http.MethodPut},
{"DELETE", Delete, http.MethodDelete},
{"PATCH", Patch, http.MethodPatch},
{"OPTIONS", Options, http.MethodOptions},
{"TRACE", Trace, http.MethodTrace},
{"CONNECT", Connect, http.MethodConnect},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
resp, err := tc.fn(s.URL)
if err != nil {
t.Fatalf("%s error: %v", tc.name, err)
}
defer resp.Close()
body, _ := resp.Body().String()
if body != tc.want {
t.Fatalf("body=%q want=%q", body, tc.want)
}
})
}
}
// ---- Request: SetQueries / SetTransport / SetAutoCalcContentLength / SetContentLength ----
func TestRequestSetQueries(t *testing.T) {
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
if q.Get("a") != "1" || q.Get("b") != "2" {
t.Fatalf("query not set: %v", q)
}
w.WriteHeader(http.StatusOK)
}))
defer s.Close()
req := NewSimpleRequest(s.URL, "GET").
SetQueries(map[string][]string{"a": {"1"}, "b": {"2"}})
resp, err := req.Do()
if err != nil {
t.Fatalf("Do() error: %v", err)
}
resp.Close()
}
func TestRequestSetTransport(t *testing.T) {
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
defer s.Close()
base := &http.Transport{}
req := NewSimpleRequest(s.URL, "GET").SetTransport(base)
resp, err := req.Do()
if err != nil {
t.Fatalf("Do() error: %v", err)
}
resp.Close()
}
func TestRequestSetAutoCalcContentLength(t *testing.T) {
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.ContentLength <= 0 {
t.Fatalf("content-length not auto calculated: %d", r.ContentLength)
}
w.WriteHeader(http.StatusOK)
}))
defer s.Close()
req := NewSimpleRequest(s.URL, "POST").
SetBodyReader(stringsNewReaderCompat("hello-autocalc")).
SetAutoCalcContentLength(true)
resp, err := req.Do()
if err != nil {
t.Fatalf("Do() error: %v", err)
}
resp.Close()
}
func TestRequestSetContentLength(t *testing.T) {
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.ContentLength != 5 {
t.Fatalf("content-length=%d", r.ContentLength)
}
w.WriteHeader(http.StatusOK)
}))
defer s.Close()
req := NewSimpleRequest(s.URL, "POST").
SetBody([]byte("hello")).
SetContentLength(5)
resp, err := req.Do()
if err != nil {
t.Fatalf("Do() error: %v", err)
}
resp.Close()
}
// ---- Request: AddCustomDNS / AddCustomIP / SetDialFunc ----
func TestRequestAddCustomDNSAndIP(t *testing.T) {
req := NewSimpleRequest("http://example.com", "GET").
AddCustomDNS("8.8.8.8").
AddCustomIP("1.1.1.1")
if req.Err() != nil {
t.Fatalf("unexpected err: %v", req.Err())
}
if len(req.config.DNS.CustomDNS) != 1 || req.config.DNS.CustomDNS[0] != "8.8.8.8" {
t.Fatal("custom dns not added")
}
if len(req.config.DNS.CustomIP) != 1 || req.config.DNS.CustomIP[0] != "1.1.1.1" {
t.Fatal("custom ip not added")
}
}
func TestRequestSetDialFunc(t *testing.T) {
called := false
fn := func(ctx context.Context, network, addr string) (net.Conn, error) {
called = true
return nil, io.EOF
}
req := NewSimpleRequest("http://example.com", "GET").SetDialFunc(fn)
if req.config.Network.DialFunc == nil {
t.Fatal("dial func not set")
}
_, _ = req.config.Network.DialFunc(context.Background(), "tcp", "x:1")
if !called {
t.Fatal("dial func not callable")
}
}
// ---- Request header/cookie bulk APIs ----
func TestRequestSetHeadersAndAddHeaders(t *testing.T) {
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("X-A") != "1" || r.Header.Get("X-B") != "2" || r.Header.Get("X-C") != "3" {
t.Fatalf("headers not correct: %v", r.Header)
}
w.WriteHeader(http.StatusOK)
}))
defer s.Close()
h := http.Header{}
h.Set("X-A", "1")
h.Set("X-B", "2")
req := NewSimpleRequest(s.URL, "GET").
SetHeaders(h).
AddHeaders(map[string]string{"X-C": "3"})
resp, err := req.Do()
if err != nil {
t.Fatalf("Do() error: %v", err)
}
resp.Close()
}
func TestRequestSetCookiesAndAddCookies(t *testing.T) {
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
got := map[string]string{}
for _, c := range r.Cookies() {
got[c.Name] = c.Value
}
if got["a"] != "1" || got["b"] != "2" || got["c"] != "3" {
t.Fatalf("cookies=%v", got)
}
w.WriteHeader(http.StatusOK)
}))
defer s.Close()
req := NewSimpleRequest(s.URL, "GET").
SetCookies([]*http.Cookie{
{Name: "a", Value: "1", Path: "/"},
{Name: "b", Value: "2", Path: "/"},
}).
AddCookies(map[string]string{"c": "3"})
resp, err := req.Do()
if err != nil {
t.Fatalf("Do() error: %v", err)
}
resp.Close()
}
// ---- Body.Close / Response.CloseWithClient ----
func TestBodyClose(t *testing.T) {
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
}))
defer s.Close()
resp, err := Get(s.URL)
if err != nil {
t.Fatalf("Get() error: %v", err)
}
// 直接测 Body.Close
if err := resp.Body().Close(); err != nil {
t.Fatalf("Body.Close() error: %v", err)
}
}
func TestResponseCloseWithClient(t *testing.T) {
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
}))
defer s.Close()
resp, err := Get(s.URL)
if err != nil {
t.Fatalf("Get() error: %v", err)
}
if err := resp.CloseWithClient(); err != nil {
t.Fatalf("CloseWithClient() error: %v", err)
}
}
// 小兼容函数,避免你当前文件没引 strings 包时报错(可直接替换成 strings.NewReader
func stringsNewReaderCompat(s string) io.Reader {
return io.NopCloser(io.MultiReader(io.LimitReader(io.NopCloser(stringsReader(s)), int64(len(s)))))
}
// 纯标准库最小 reader
type stringsReader string
func (sr stringsReader) Read(p []byte) (int, error) {
if len(sr) == 0 {
return 0, io.EOF
}
n := copy(p, []byte(sr))
return n, nil
}