61819ac3e3
PR Ready Gate / clear-labels (pull_request) Successful in 2s
CI / test (pull_request) Successful in 17s
CI / review (anthropic--claude-4.6-sonnet, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 36s
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 1m35s
CI / review (gpt-5, security, ., rodin/security-patterns, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 2m7s
- MAJOR #1: Replace panic in doRequest with safe default fallback. Validation now happens in SetRetryBackoff (returns error on invalid length). doRequest gracefully falls back to default backoff if the configured slice is somehow invalid. - MINOR #2: SetRetryBackoff validates slice length at configuration time, making the coupling between maxRetryAttempts and backoff explicit and catching mismatches early with a clear error. - MINOR #4: Reword oversized response error to remove '(truncated)' which implied truncated data was returned when actually only an error is returned. - MINOR #5: Functional options kept as-is - idiomatic Go pattern that allows future growth without breaking the API.
595 lines
18 KiB
Go
595 lines
18 KiB
Go
package github
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestNewClient_DefaultBaseURL(t *testing.T) {
|
|
c := NewClient("test-token", "")
|
|
if c.baseURL != "https://api.github.com" {
|
|
t.Errorf("expected default base URL, got %q", c.baseURL)
|
|
}
|
|
}
|
|
|
|
func TestNewClient_CustomBaseURL(t *testing.T) {
|
|
c := NewClient("test-token", "https://github.concur.com/api/v3")
|
|
if c.baseURL != "https://github.concur.com/api/v3" {
|
|
t.Errorf("expected custom base URL, got %q", c.baseURL)
|
|
}
|
|
}
|
|
|
|
func TestNewClient_TrimsTrailingSlash(t *testing.T) {
|
|
c := NewClient("test-token", "https://github.concur.com/api/v3/")
|
|
if c.baseURL != "https://github.concur.com/api/v3" {
|
|
t.Errorf("expected trailing slash trimmed, got %q", c.baseURL)
|
|
}
|
|
}
|
|
|
|
func TestDoRequest_SetsAuthHeader(t *testing.T) {
|
|
var gotAuth string
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
gotAuth = r.Header.Get("Authorization")
|
|
w.WriteHeader(200)
|
|
w.Write([]byte("{}"))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
c := NewClient("my-token", srv.URL, AllowInsecureHTTP())
|
|
c.SetHTTPClient(srv.Client())
|
|
_, _ = c.doGet(context.Background(), srv.URL+"/test")
|
|
|
|
if gotAuth != "Bearer my-token" {
|
|
t.Errorf("expected Bearer auth, got %q", gotAuth)
|
|
}
|
|
}
|
|
|
|
func TestDoRequest_SetsDefaultAcceptHeader(t *testing.T) {
|
|
var gotAccept string
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
gotAccept = r.Header.Get("Accept")
|
|
w.WriteHeader(200)
|
|
w.Write([]byte("{}"))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
c := NewClient("token", srv.URL, AllowInsecureHTTP())
|
|
c.SetHTTPClient(srv.Client())
|
|
_, _ = c.doGet(context.Background(), srv.URL+"/test")
|
|
|
|
if gotAccept != "application/vnd.github+json" {
|
|
t.Errorf("expected default Accept header, got %q", gotAccept)
|
|
}
|
|
}
|
|
|
|
func TestDoRequest_429Retry(t *testing.T) {
|
|
attempts := 0
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
attempts++
|
|
if attempts == 1 {
|
|
w.WriteHeader(429)
|
|
w.Write([]byte(`{"message":"rate limit"}`))
|
|
return
|
|
}
|
|
w.WriteHeader(200)
|
|
w.Write([]byte(`{"ok":true}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
c := NewClient("token", srv.URL, AllowInsecureHTTP())
|
|
c.SetHTTPClient(srv.Client())
|
|
if err := c.SetRetryBackoff([]time.Duration{10 * time.Millisecond, 10 * time.Millisecond}); err != nil {
|
|
t.Fatalf("SetRetryBackoff: %v", err)
|
|
}
|
|
|
|
body, err := c.doGet(context.Background(), srv.URL+"/test")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if string(body) != `{"ok":true}` {
|
|
t.Errorf("unexpected body: %s", body)
|
|
}
|
|
if attempts != 2 {
|
|
t.Errorf("expected 2 attempts, got %d", attempts)
|
|
}
|
|
}
|
|
|
|
func TestDoRequest_429ExhaustsRetries(t *testing.T) {
|
|
attempts := 0
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
attempts++
|
|
w.WriteHeader(429)
|
|
w.Write([]byte(`{"message":"rate limit"}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
c := NewClient("token", srv.URL, AllowInsecureHTTP())
|
|
c.SetHTTPClient(srv.Client())
|
|
if err := c.SetRetryBackoff([]time.Duration{1 * time.Millisecond, 1 * time.Millisecond}); err != nil {
|
|
t.Fatalf("SetRetryBackoff: %v", err)
|
|
}
|
|
|
|
_, err := c.doGet(context.Background(), srv.URL+"/test")
|
|
if err == nil {
|
|
t.Fatal("expected error after exhausting retries")
|
|
}
|
|
apiErr, ok := err.(*APIError)
|
|
if !ok {
|
|
t.Fatalf("expected *APIError, got %T", err)
|
|
}
|
|
if apiErr.StatusCode != 429 {
|
|
t.Errorf("expected 429, got %d", apiErr.StatusCode)
|
|
}
|
|
if attempts != 3 {
|
|
t.Errorf("expected 3 attempts, got %d", attempts)
|
|
}
|
|
}
|
|
|
|
func TestDoRequest_404NoRetry(t *testing.T) {
|
|
attempts := 0
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
attempts++
|
|
w.WriteHeader(404)
|
|
w.Write([]byte(`{"message":"not found"}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
c := NewClient("token", srv.URL, AllowInsecureHTTP())
|
|
c.SetHTTPClient(srv.Client())
|
|
|
|
_, err := c.doGet(context.Background(), srv.URL+"/test")
|
|
if err == nil {
|
|
t.Fatal("expected error for 404")
|
|
}
|
|
if attempts != 1 {
|
|
t.Errorf("expected 1 attempt (no retry on 404), got %d", attempts)
|
|
}
|
|
}
|
|
|
|
func TestDoRequest_401NoRetry(t *testing.T) {
|
|
attempts := 0
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
attempts++
|
|
w.WriteHeader(401)
|
|
w.Write([]byte(`{"message":"bad credentials"}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
c := NewClient("token", srv.URL, AllowInsecureHTTP())
|
|
c.SetHTTPClient(srv.Client())
|
|
|
|
_, err := c.doGet(context.Background(), srv.URL+"/test")
|
|
if err == nil {
|
|
t.Fatal("expected error for 401")
|
|
}
|
|
if attempts != 1 {
|
|
t.Errorf("expected 1 attempt (no retry on 401), got %d", attempts)
|
|
}
|
|
}
|
|
|
|
func TestIsNotFound(t *testing.T) {
|
|
err := &APIError{StatusCode: 404, Body: "not found"}
|
|
if !IsNotFound(err) {
|
|
t.Error("expected IsNotFound to return true for 404")
|
|
}
|
|
err2 := &APIError{StatusCode: 500, Body: "server error"}
|
|
if IsNotFound(err2) {
|
|
t.Error("expected IsNotFound to return false for 500")
|
|
}
|
|
}
|
|
|
|
func TestIsUnauthorized(t *testing.T) {
|
|
err := &APIError{StatusCode: 401, Body: "bad credentials"}
|
|
if !IsUnauthorized(err) {
|
|
t.Error("expected IsUnauthorized to return true for 401")
|
|
}
|
|
}
|
|
|
|
func TestAPIError_SanitizesNewlines(t *testing.T) {
|
|
err := &APIError{StatusCode: 500, Body: "line1\ninjected\rmore"}
|
|
msg := err.Error()
|
|
if strings.Contains(msg, "\n") || strings.Contains(msg, "\r") {
|
|
t.Errorf("expected newlines to be sanitized, got: %q", msg)
|
|
}
|
|
if !strings.Contains(msg, "line1 injected more") {
|
|
t.Errorf("expected sanitized body, got: %q", msg)
|
|
}
|
|
}
|
|
|
|
func TestDoRequest_429RetryAfterHeader(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping slow retry test in short mode")
|
|
}
|
|
attempts := 0
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
attempts++
|
|
if attempts == 1 {
|
|
w.Header().Set("Retry-After", "1")
|
|
w.WriteHeader(429)
|
|
w.Write([]byte(`{"message":"rate limit"}`))
|
|
return
|
|
}
|
|
w.WriteHeader(200)
|
|
w.Write([]byte(`{"ok":true}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
c := NewClient("token", srv.URL, AllowInsecureHTTP())
|
|
c.SetHTTPClient(srv.Client())
|
|
// Use short backoff; Retry-After should override
|
|
if err := c.SetRetryBackoff([]time.Duration{1 * time.Millisecond, 1 * time.Millisecond}); err != nil {
|
|
t.Fatalf("SetRetryBackoff: %v", err)
|
|
}
|
|
|
|
start := time.Now()
|
|
body, err := c.doGet(context.Background(), srv.URL+"/test")
|
|
elapsed := time.Since(start)
|
|
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if string(body) != `{"ok":true}` {
|
|
t.Errorf("unexpected body: %s", body)
|
|
}
|
|
if attempts != 2 {
|
|
t.Errorf("expected 2 attempts, got %d", attempts)
|
|
}
|
|
// Retry-After: 1 means at least 1 second delay
|
|
if elapsed < 900*time.Millisecond {
|
|
t.Errorf("expected ~1s delay from Retry-After, got %v", elapsed)
|
|
}
|
|
}
|
|
|
|
func TestDoRequest_RetryAfterDoesNotMutateBackoff(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping slow retry test in short mode")
|
|
}
|
|
attempts := 0
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
attempts++
|
|
if attempts == 1 {
|
|
w.Header().Set("Retry-After", "1")
|
|
w.WriteHeader(429)
|
|
w.Write([]byte(`{"message":"rate limit"}`))
|
|
return
|
|
}
|
|
w.WriteHeader(200)
|
|
w.Write([]byte(`{"ok":true}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
c := NewClient("token", srv.URL, AllowInsecureHTTP())
|
|
c.SetHTTPClient(srv.Client())
|
|
if err := c.SetRetryBackoff([]time.Duration{1 * time.Millisecond, 1 * time.Millisecond}); err != nil {
|
|
t.Fatalf("SetRetryBackoff: %v", err)
|
|
}
|
|
|
|
_, err := c.doGet(context.Background(), srv.URL+"/test")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
// Verify the original retryBackoff slice was not mutated
|
|
if c.retryBackoff[0] != 1*time.Millisecond {
|
|
t.Errorf("retryBackoff[0] was mutated: got %v, want 1ms", c.retryBackoff[0])
|
|
}
|
|
if c.retryBackoff[1] != 1*time.Millisecond {
|
|
t.Errorf("retryBackoff[1] was mutated: got %v, want 1ms", c.retryBackoff[1])
|
|
}
|
|
}
|
|
|
|
func TestDoRequest_429RetryAfterHTTPDate(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping slow Retry-After HTTP-date test in short mode")
|
|
}
|
|
attempts := 0
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
attempts++
|
|
if attempts == 1 {
|
|
// Use HTTP-date format (RFC 7231) — a time 2 seconds in the future.
|
|
future := time.Now().Add(2 * time.Second).UTC()
|
|
w.Header().Set("Retry-After", future.Format(http.TimeFormat))
|
|
w.WriteHeader(429)
|
|
w.Write([]byte(`{"message":"rate limit"}`))
|
|
return
|
|
}
|
|
w.WriteHeader(200)
|
|
w.Write([]byte(`{"ok":true}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
c := NewClient("token", srv.URL, AllowInsecureHTTP())
|
|
c.SetHTTPClient(srv.Client())
|
|
if err := c.SetRetryBackoff([]time.Duration{1 * time.Millisecond, 1 * time.Millisecond}); err != nil {
|
|
t.Fatalf("SetRetryBackoff: %v", err)
|
|
}
|
|
|
|
start := time.Now()
|
|
body, err := c.doGet(context.Background(), srv.URL+"/test")
|
|
elapsed := time.Since(start)
|
|
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if string(body) != `{"ok":true}` {
|
|
t.Errorf("unexpected body: %s", body)
|
|
}
|
|
if attempts != 2 {
|
|
t.Errorf("expected 2 attempts, got %d", attempts)
|
|
}
|
|
// HTTP-date was ~2s in the future; by the time client processes it,
|
|
// time.Until gives ~1-2s. Verify it's meaningfully delayed (not instant).
|
|
if elapsed < 500*time.Millisecond {
|
|
t.Errorf("expected meaningful delay from HTTP-date Retry-After, got %v", elapsed)
|
|
}
|
|
}
|
|
|
|
func TestDoRequest_429RetryAfterHTTPDateInPast(t *testing.T) {
|
|
attempts := 0
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
attempts++
|
|
if attempts == 1 {
|
|
// Use a time in the past — should result in zero/immediate retry.
|
|
past := time.Now().Add(-10 * time.Second).UTC()
|
|
w.Header().Set("Retry-After", past.Format(http.TimeFormat))
|
|
w.WriteHeader(429)
|
|
w.Write([]byte(`{"message":"rate limit"}`))
|
|
return
|
|
}
|
|
w.WriteHeader(200)
|
|
w.Write([]byte(`{"ok":true}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
c := NewClient("token", srv.URL, AllowInsecureHTTP())
|
|
c.SetHTTPClient(srv.Client())
|
|
if err := c.SetRetryBackoff([]time.Duration{5 * time.Second, 5 * time.Second}); err != nil {
|
|
t.Fatalf("SetRetryBackoff: %v", err)
|
|
}
|
|
|
|
start := time.Now()
|
|
_, err := c.doGet(context.Background(), srv.URL+"/test")
|
|
elapsed := time.Since(start)
|
|
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if attempts != 2 {
|
|
t.Errorf("expected 2 attempts, got %d", attempts)
|
|
}
|
|
// Past date should override the 5s backoff to ~0
|
|
if elapsed > 500*time.Millisecond {
|
|
t.Errorf("expected near-instant retry for past HTTP-date, got %v", elapsed)
|
|
}
|
|
}
|
|
|
|
func TestDoRequest_SetsUserAgentHeader(t *testing.T) {
|
|
var gotUA string
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
gotUA = r.Header.Get("User-Agent")
|
|
w.WriteHeader(200)
|
|
w.Write([]byte("{}"))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
c := NewClient("token", srv.URL, AllowInsecureHTTP())
|
|
c.SetHTTPClient(srv.Client())
|
|
_, _ = c.doGet(context.Background(), srv.URL+"/test")
|
|
|
|
if gotUA != "review-bot/1.0" {
|
|
t.Errorf("expected User-Agent 'review-bot/1.0', got %q", gotUA)
|
|
}
|
|
}
|
|
|
|
func TestDoRequest_LimitsResponseBody(t *testing.T) {
|
|
// Verify that oversized responses return an error rather than silently truncating.
|
|
bigBody := strings.Repeat("x", maxResponseBytes+1024)
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(200)
|
|
w.Write([]byte(bigBody))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
c := NewClient("token", srv.URL, AllowInsecureHTTP())
|
|
c.SetHTTPClient(srv.Client())
|
|
_, err := c.doGet(context.Background(), srv.URL+"/test")
|
|
if err == nil {
|
|
t.Fatal("expected error for oversized response body")
|
|
}
|
|
if !strings.Contains(err.Error(), "exceeded") {
|
|
t.Errorf("expected truncation error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDoRequest_AcceptsExactlyAtLimit(t *testing.T) {
|
|
// A response body exactly equal to maxResponseBytes should succeed (not error).
|
|
exactBody := strings.Repeat("x", maxResponseBytes)
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(200)
|
|
w.Write([]byte(exactBody))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
c := NewClient("token", srv.URL, AllowInsecureHTTP())
|
|
c.SetHTTPClient(srv.Client())
|
|
body, err := c.doGet(context.Background(), srv.URL+"/test")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error for exactly-at-limit body: %v", err)
|
|
}
|
|
if len(body) != maxResponseBytes {
|
|
t.Errorf("expected body length %d, got %d", maxResponseBytes, len(body))
|
|
}
|
|
}
|
|
|
|
func TestDoRequest_SkipsAuthWhenTokenEmpty(t *testing.T) {
|
|
var gotAuth string
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
gotAuth = r.Header.Get("Authorization")
|
|
w.WriteHeader(200)
|
|
w.Write([]byte("{}"))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
c := NewClient("", srv.URL, AllowInsecureHTTP()) // empty token
|
|
c.SetHTTPClient(srv.Client())
|
|
_, _ = c.doGet(context.Background(), srv.URL+"/test")
|
|
|
|
if gotAuth != "" {
|
|
t.Errorf("expected no Authorization header with empty token, got %q", gotAuth)
|
|
}
|
|
}
|
|
|
|
func TestNewClient_CheckRedirectStripsAuthOnCrossHost(t *testing.T) {
|
|
// Verify the CheckRedirect function is configured
|
|
c := NewClient("secret-token", "https://api.github.com")
|
|
if c.httpClient.CheckRedirect == nil {
|
|
t.Fatal("expected CheckRedirect to be set")
|
|
}
|
|
}
|
|
|
|
func TestDefaultCheckRedirect_RejectsHTTPSToHTTP(t *testing.T) {
|
|
prev := &http.Request{URL: &url.URL{Scheme: "https", Host: "api.github.com", Path: "/foo"}}
|
|
req := &http.Request{
|
|
URL: &url.URL{Scheme: "http", Host: "api.github.com", Path: "/foo"},
|
|
Header: http.Header{"Authorization": []string{"Bearer token"}},
|
|
}
|
|
err := defaultCheckRedirect(req, []*http.Request{prev})
|
|
if err == nil {
|
|
t.Fatal("expected error on HTTPS→HTTP redirect")
|
|
}
|
|
if !strings.Contains(err.Error(), "refusing redirect from HTTPS to HTTP") {
|
|
t.Errorf("unexpected error message: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDefaultCheckRedirect_StripsAuthOnCrossHost(t *testing.T) {
|
|
prev := &http.Request{URL: &url.URL{Scheme: "https", Host: "api.github.com", Path: "/foo"}}
|
|
req := &http.Request{
|
|
URL: &url.URL{Scheme: "https", Host: "objects.githubusercontent.com", Path: "/bar"},
|
|
Header: http.Header{"Authorization": []string{"Bearer token"}},
|
|
}
|
|
err := defaultCheckRedirect(req, []*http.Request{prev})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if auth := req.Header.Get("Authorization"); auth != "" {
|
|
t.Errorf("expected Authorization header to be stripped, got %q", auth)
|
|
}
|
|
}
|
|
|
|
func TestDefaultCheckRedirect_PreservesAuthOnSameHost(t *testing.T) {
|
|
prev := &http.Request{URL: &url.URL{Scheme: "https", Host: "api.github.com", Path: "/foo"}}
|
|
req := &http.Request{
|
|
URL: &url.URL{Scheme: "https", Host: "api.github.com", Path: "/bar"},
|
|
Header: http.Header{"Authorization": []string{"Bearer token"}},
|
|
}
|
|
err := defaultCheckRedirect(req, []*http.Request{prev})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if auth := req.Header.Get("Authorization"); auth != "Bearer token" {
|
|
t.Errorf("expected Authorization to be preserved, got %q", auth)
|
|
}
|
|
}
|
|
|
|
func TestDoRequest_RejectsHTTPWithToken(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(200)
|
|
w.Write([]byte("{}"))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
// Without AllowInsecureHTTP, should refuse to send token over HTTP
|
|
c := NewClient("secret-token", srv.URL)
|
|
c.SetHTTPClient(srv.Client())
|
|
_, err := c.doGet(context.Background(), srv.URL+"/test")
|
|
if err == nil {
|
|
t.Fatal("expected error when sending token over HTTP")
|
|
}
|
|
if !strings.Contains(err.Error(), "refusing to send credentials") {
|
|
t.Errorf("unexpected error message: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDoRequest_AllowsHTTPWithoutToken(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(200)
|
|
w.Write([]byte(`{"ok":true}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
// Without token, HTTP should be fine (no credentials to leak)
|
|
c := NewClient("", srv.URL)
|
|
c.SetHTTPClient(srv.Client())
|
|
body, err := c.doGet(context.Background(), srv.URL+"/test")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if string(body) != `{"ok":true}` {
|
|
t.Errorf("unexpected body: %s", body)
|
|
}
|
|
}
|
|
|
|
func TestDoRequest_AllowsHTTPWithInsecureOption(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(200)
|
|
w.Write([]byte(`{"ok":true}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
c := NewClient("secret-token", srv.URL, AllowInsecureHTTP())
|
|
c.SetHTTPClient(srv.Client())
|
|
body, err := c.doGet(context.Background(), srv.URL+"/test")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if string(body) != `{"ok":true}` {
|
|
t.Errorf("unexpected body: %s", body)
|
|
}
|
|
}
|
|
|
|
func TestSetHTTPClient_NilRestoresDefault(t *testing.T) {
|
|
c := NewClient("token", "https://api.github.com")
|
|
c.SetHTTPClient(nil)
|
|
if c.httpClient == nil {
|
|
t.Fatal("expected non-nil httpClient after SetHTTPClient(nil)")
|
|
}
|
|
if c.httpClient.Timeout != 30*time.Second {
|
|
t.Errorf("expected 30s timeout, got %v", c.httpClient.Timeout)
|
|
}
|
|
if c.httpClient.CheckRedirect == nil {
|
|
t.Fatal("expected CheckRedirect policy after SetHTTPClient(nil)")
|
|
}
|
|
}
|
|
|
|
|
|
func TestSetRetryBackoff_RejectsInvalidLength(t *testing.T) {
|
|
c := NewClient("token", "https://api.github.com")
|
|
|
|
// Too short
|
|
err := c.SetRetryBackoff([]time.Duration{1 * time.Second})
|
|
if err == nil {
|
|
t.Fatal("expected error for backoff length 1")
|
|
}
|
|
if !strings.Contains(err.Error(), "backoff length 1") {
|
|
t.Errorf("unexpected error message: %v", err)
|
|
}
|
|
|
|
// Too long
|
|
err = c.SetRetryBackoff([]time.Duration{1 * time.Second, 2 * time.Second, 3 * time.Second})
|
|
if err == nil {
|
|
t.Fatal("expected error for backoff length 3")
|
|
}
|
|
|
|
// Correct length succeeds
|
|
err = c.SetRetryBackoff([]time.Duration{1 * time.Second, 2 * time.Second})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error for valid backoff: %v", err)
|
|
}
|
|
}
|