github.com/anthropics/anthropic-sdk-go@v1.70.1/internal/auth/middleware_test.go (about)

     1  package auth
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"errors"
     7  	"io"
     8  	"log"
     9  	"net/http"
    10  	"net/http/httptest"
    11  	"net/url"
    12  	"os"
    13  	"strings"
    14  	"testing"
    15  
    16  	"github.com/anthropics/anthropic-sdk-go/internal/requestconfig"
    17  )
    18  
    19  func configFor(t *testing.T, base string) *requestconfig.RequestConfig {
    20  	t.Helper()
    21  	u, err := url.Parse(base)
    22  	if err != nil {
    23  		t.Fatal(err)
    24  	}
    25  	return &requestconfig.RequestConfig{BaseURL: u}
    26  }
    27  
    28  // TestMiddleware_401WithUnreplayableBodyWarns verifies that the 401-retry path
    29  // emits a one-shot warning when it has to give up because the request body
    30  // cannot be replayed (no GetBody). Today this returns the 401 silently.
    31  func TestMiddleware_401WithUnreplayableBodyWarns(t *testing.T) {
    32  	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    33  		w.WriteHeader(http.StatusUnauthorized)
    34  		w.Write([]byte(`{}`))
    35  	}))
    36  	defer server.Close()
    37  
    38  	provider := func(_ context.Context, _ string, _ func(*http.Request) (*http.Response, error)) (*AccessToken, error) {
    39  		return &AccessToken{Token: "tok-1"}, nil
    40  	}
    41  	cache := NewTokenCache(provider, http.DefaultClient.Do)
    42  	mw := NewMiddleware(cache, configFor(t, server.URL))
    43  
    44  	req, _ := http.NewRequestWithContext(context.Background(), http.MethodPost, server.URL, io.NopCloser(strings.NewReader("body")))
    45  	req.GetBody = nil // unreplayable
    46  
    47  	resetWarnOnceForTest(t)
    48  	var buf syncBuffer
    49  	log.SetOutput(&buf)
    50  	defer log.SetOutput(os.Stderr)
    51  
    52  	resp, err := mw(req, http.DefaultClient.Do)
    53  	if err != nil {
    54  		t.Fatalf("expected nil error, got %v", err)
    55  	}
    56  	if resp.StatusCode != http.StatusUnauthorized {
    57  		t.Fatalf("expected 401, got %d", resp.StatusCode)
    58  	}
    59  	resp.Body.Close()
    60  
    61  	if !strings.Contains(buf.String(), "cannot be replayed") {
    62  		t.Fatalf("expected warning about unreplayable body, got: %q", buf.String())
    63  	}
    64  }
    65  
    66  // TestMiddleware_401ForceTokenErrorWarns verifies that when ForceToken itself
    67  // returns an error during the 401 retry, the middleware logs the underlying
    68  // cause instead of silently returning the original 401.
    69  func TestMiddleware_401ForceTokenErrorWarns(t *testing.T) {
    70  	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    71  		w.WriteHeader(http.StatusUnauthorized)
    72  		w.Write([]byte(`{}`))
    73  	}))
    74  	defer server.Close()
    75  
    76  	var callCount int
    77  	forceErr := errors.New("token endpoint unreachable at http://elsewhere")
    78  	provider := func(_ context.Context, _ string, _ func(*http.Request) (*http.Response, error)) (*AccessToken, error) {
    79  		callCount++
    80  		if callCount == 1 {
    81  			return &AccessToken{Token: "tok-1"}, nil
    82  		}
    83  		return nil, forceErr
    84  	}
    85  	cache := NewTokenCache(provider, http.DefaultClient.Do)
    86  	mw := NewMiddleware(cache, configFor(t, server.URL))
    87  
    88  	req, _ := http.NewRequestWithContext(context.Background(), http.MethodPost, server.URL, bytes.NewReader([]byte("body")))
    89  	req.GetBody = func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader([]byte("body"))), nil }
    90  
    91  	resetWarnOnceForTest(t)
    92  	var buf syncBuffer
    93  	log.SetOutput(&buf)
    94  	defer log.SetOutput(os.Stderr)
    95  
    96  	resp, err := mw(req, http.DefaultClient.Do)
    97  	if err != nil {
    98  		t.Fatalf("expected nil error from middleware so caller gets the original 401, got %v", err)
    99  	}
   100  	if resp.StatusCode != http.StatusUnauthorized {
   101  		t.Fatalf("expected 401, got %d", resp.StatusCode)
   102  	}
   103  	resp.Body.Close()
   104  
   105  	out := buf.String()
   106  	if !strings.Contains(out, "token endpoint unreachable") {
   107  		t.Fatalf("expected warning to include underlying error, got: %q", out)
   108  	}
   109  }
   110  
   111  func TestMiddleware_ExchangesAtConfiguredBaseURL(t *testing.T) {
   112  	var providerBaseURLs []string
   113  	provider := func(_ context.Context, baseURL string, _ func(*http.Request) (*http.Response, error)) (*AccessToken, error) {
   114  		providerBaseURLs = append(providerBaseURLs, baseURL)
   115  		return &AccessToken{Token: "tok-1"}, nil
   116  	}
   117  	next := func(req *http.Request) (*http.Response, error) {
   118  		return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody, Request: req}, nil
   119  	}
   120  	mw := NewMiddleware(NewTokenCache(provider, nil), configFor(t, "https://api.example.test/prefix/"))
   121  
   122  	req, _ := http.NewRequest(http.MethodGet, "https://API.example.test/prefix/v1/models", nil)
   123  	if _, err := mw(req, next); err != nil {
   124  		t.Fatal(err)
   125  	}
   126  	if got := req.Header.Get("Authorization"); got != "Bearer tok-1" {
   127  		t.Fatalf("same-origin request got Authorization %q, want bearer", got)
   128  	}
   129  	if len(providerBaseURLs) != 1 || providerBaseURLs[0] != "https://api.example.test" {
   130  		t.Fatalf("provider saw base URLs %q, want just the configured origin", providerBaseURLs)
   131  	}
   132  
   133  	for _, foreign := range []string{
   134  		"https://attacker.example.test/v1/models",
   135  		"https://api.example.test:8443/v1/models",
   136  		"http://api.example.test/v1/models",
   137  	} {
   138  		req, _ := http.NewRequest(http.MethodGet, foreign, nil)
   139  		if _, err := mw(req, next); err != nil {
   140  			t.Fatalf("%s: %v", foreign, err)
   141  		}
   142  		if got := req.Header.Get("Authorization"); got != "" {
   143  			t.Errorf("%s: Authorization %q attached to a foreign origin", foreign, got)
   144  		}
   145  		if got := req.Header.Get("anthropic-beta"); got != "" {
   146  			t.Errorf("%s: anthropic-beta %q attached to a foreign origin", foreign, got)
   147  		}
   148  	}
   149  	if len(providerBaseURLs) != 1 {
   150  		t.Fatalf("provider was re-invoked for foreign origins: %q", providerBaseURLs)
   151  	}
   152  }
   153  
   154  func TestMiddleware_UnresolvedBaseURLFails(t *testing.T) {
   155  	provider := func(_ context.Context, _ string, _ func(*http.Request) (*http.Response, error)) (*AccessToken, error) {
   156  		t.Fatal("provider must not be invoked without a configured base URL")
   157  		return nil, nil
   158  	}
   159  	mw := NewMiddleware(NewTokenCache(provider, nil), &requestconfig.RequestConfig{})
   160  	req, _ := http.NewRequest(http.MethodGet, "https://api.example.test/v1/models", nil)
   161  	_, err := mw(req, func(*http.Request) (*http.Response, error) {
   162  		t.Fatal("request must not be sent")
   163  		return nil, nil
   164  	})
   165  	if err == nil || !strings.Contains(err.Error(), "base URL is not set") {
   166  		t.Fatalf("got %v, want base URL error", err)
   167  	}
   168  }