test: add comprehensive OAuth token tests and refactor
Add comprehensive tests for OAuth token detection and authentication: - Token detection tests (OAuth vs standard API keys, whitespace handling) - Provider initialization tests (OAuth and standard tokens) - Token source refresh tests (OAuth and standard tokens) - Whitespace trimming verification Refactor to eliminate code duplication: - Extract `appendAuthOptions` helper function - Centralizes OAuth vs standard token logic in one place - Ensures consistent behavior across NewProviderWithBaseURL and Chat methods - Properly trims whitespace from all tokens before use All tests verify: - OAuth tokens (sk-ant-oat01-*) use Bearer auth + anthropic-beta header - Standard API keys use Bearer auth without beta header - Whitespace is trimmed correctly - Token refresh via tokenSource works for both token types Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
ba2b4da723
commit
2c171c2600
2 changed files with 276 additions and 21 deletions
|
|
@ -30,6 +30,20 @@ func isOAuthToken(token string) bool {
|
|||
return strings.HasPrefix(strings.TrimSpace(token), "sk-ant-oat01-")
|
||||
}
|
||||
|
||||
// appendAuthOptions appends authentication options based on token type.
|
||||
// OAuth tokens (sk-ant-oat01-*) use Bearer authentication with beta header.
|
||||
// Standard API keys use x-api-key header.
|
||||
func appendAuthOptions(opts []option.RequestOption, token string) []option.RequestOption {
|
||||
token = strings.TrimSpace(token)
|
||||
if isOAuthToken(token) {
|
||||
return append(opts,
|
||||
option.WithHeader("Authorization", "Bearer "+token),
|
||||
option.WithHeader("anthropic-beta", "oauth-2025-04-20"),
|
||||
)
|
||||
}
|
||||
return append(opts, option.WithAuthToken(token))
|
||||
}
|
||||
|
||||
type Provider struct {
|
||||
client *anthropic.Client
|
||||
tokenSource func() (string, error)
|
||||
|
|
@ -44,17 +58,7 @@ func NewProviderWithBaseURL(token, apiBase string) *Provider {
|
|||
baseURL := normalizeBaseURL(apiBase)
|
||||
|
||||
var opts []option.RequestOption
|
||||
|
||||
// OAuth tokens (Claude Code) require special handling
|
||||
if isOAuthToken(token) {
|
||||
opts = append(opts,
|
||||
option.WithHeader("Authorization", "Bearer "+token),
|
||||
option.WithHeader("anthropic-beta", "oauth-2025-04-20"),
|
||||
)
|
||||
} else {
|
||||
opts = append(opts, option.WithAuthToken(token))
|
||||
}
|
||||
|
||||
opts = appendAuthOptions(opts, token)
|
||||
opts = append(opts, option.WithBaseURL(baseURL))
|
||||
|
||||
client := anthropic.NewClient(opts...)
|
||||
|
|
@ -94,16 +98,7 @@ func (p *Provider) Chat(
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("refreshing token: %w", err)
|
||||
}
|
||||
|
||||
// OAuth tokens (Claude Code) require special handling
|
||||
if isOAuthToken(tok) {
|
||||
opts = append(opts,
|
||||
option.WithHeader("Authorization", "Bearer "+tok),
|
||||
option.WithHeader("anthropic-beta", "oauth-2025-04-20"),
|
||||
)
|
||||
} else {
|
||||
opts = append(opts, option.WithAuthToken(tok))
|
||||
}
|
||||
opts = appendAuthOptions(opts, tok)
|
||||
}
|
||||
|
||||
params, err := buildParams(messages, tools, model, options)
|
||||
|
|
|
|||
260
pkg/providers/anthropic/provider_oauth_test.go
Normal file
260
pkg/providers/anthropic/provider_oauth_test.go
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
package anthropicprovider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIsOAuthToken_DetectsClaudeCodeTokens(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
token string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "OAuth token with sk-ant-oat01- prefix",
|
||||
token: "sk-ant-oat01-abcdefghijk",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "OAuth token with whitespace",
|
||||
token: " sk-ant-oat01-xyz123 ",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Standard API key sk-ant-api03-",
|
||||
token: "sk-ant-api03-standard-key",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "Empty token",
|
||||
token: "",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "Whitespace only",
|
||||
token: " ",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "Different prefix",
|
||||
token: "sk-ant-other-prefix",
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := isOAuthToken(tt.token)
|
||||
if got != tt.want {
|
||||
t.Errorf("isOAuthToken(%q) = %v, want %v", tt.token, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewProviderWithBaseURL_OAuthToken_UsesBearerAuth(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Verify OAuth-specific headers
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader != "Bearer sk-ant-oat01-test-token" {
|
||||
t.Errorf("Authorization header = %q, want %q", authHeader, "Bearer sk-ant-oat01-test-token")
|
||||
}
|
||||
|
||||
betaHeader := r.Header.Get("anthropic-beta")
|
||||
if betaHeader != "oauth-2025-04-20" {
|
||||
t.Errorf("anthropic-beta header = %q, want %q", betaHeader, "oauth-2025-04-20")
|
||||
}
|
||||
|
||||
// Should NOT have x-api-key header
|
||||
if apiKey := r.Header.Get("x-api-key"); apiKey != "" {
|
||||
t.Errorf("x-api-key header should not be set, got %q", apiKey)
|
||||
}
|
||||
|
||||
resp := map[string]any{
|
||||
"id": "msg_test",
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"model": "claude-sonnet-4.6",
|
||||
"stop_reason": "end_turn",
|
||||
"content": []map[string]any{
|
||||
{"type": "text", "text": "OAuth works"},
|
||||
},
|
||||
"usage": map[string]any{
|
||||
"input_tokens": 5,
|
||||
"output_tokens": 2,
|
||||
},
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
provider := NewProviderWithBaseURL("sk-ant-oat01-test-token", server.URL)
|
||||
_, err := provider.Chat(context.Background(), []Message{{Role: "user", Content: "test"}}, nil, "claude-sonnet-4.6", map[string]any{"max_tokens": 100})
|
||||
if err != nil {
|
||||
t.Fatalf("Chat() error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewProviderWithBaseURL_StandardAPIKey_UsesStandardAuth(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Verify standard API key uses Authorization header without beta
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader != "Bearer sk-ant-api03-standard-key" {
|
||||
t.Errorf("Authorization header = %q, want %q", authHeader, "Bearer sk-ant-api03-standard-key")
|
||||
}
|
||||
|
||||
// Should NOT have OAuth beta header
|
||||
if betaHeader := r.Header.Get("anthropic-beta"); betaHeader != "" {
|
||||
t.Errorf("anthropic-beta header should not be set for standard key, got %q", betaHeader)
|
||||
}
|
||||
|
||||
resp := map[string]any{
|
||||
"id": "msg_test",
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"model": "claude-sonnet-4.6",
|
||||
"stop_reason": "end_turn",
|
||||
"content": []map[string]any{
|
||||
{"type": "text", "text": "Standard auth works"},
|
||||
},
|
||||
"usage": map[string]any{
|
||||
"input_tokens": 5,
|
||||
"output_tokens": 3,
|
||||
},
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
provider := NewProviderWithBaseURL("sk-ant-api03-standard-key", server.URL)
|
||||
_, err := provider.Chat(context.Background(), []Message{{Role: "user", Content: "test"}}, nil, "claude-sonnet-4.6", map[string]any{"max_tokens": 100})
|
||||
if err != nil {
|
||||
t.Fatalf("Chat() error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenSource_WithOAuthToken_UsesBearerAuth(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Verify OAuth headers from refreshed token
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader != "Bearer sk-ant-oat01-refreshed" {
|
||||
t.Errorf("Authorization header = %q, want %q", authHeader, "Bearer sk-ant-oat01-refreshed")
|
||||
}
|
||||
|
||||
betaHeader := r.Header.Get("anthropic-beta")
|
||||
if betaHeader != "oauth-2025-04-20" {
|
||||
t.Errorf("anthropic-beta header = %q, want %q", betaHeader, "oauth-2025-04-20")
|
||||
}
|
||||
|
||||
resp := map[string]any{
|
||||
"id": "msg_test",
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"model": "claude-sonnet-4.6",
|
||||
"stop_reason": "end_turn",
|
||||
"content": []map[string]any{
|
||||
{"type": "text", "text": "Refreshed OAuth works"},
|
||||
},
|
||||
"usage": map[string]any{
|
||||
"input_tokens": 5,
|
||||
"output_tokens": 3,
|
||||
},
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tokenSource := func() (string, error) {
|
||||
return "sk-ant-oat01-refreshed", nil
|
||||
}
|
||||
|
||||
provider := NewProviderWithTokenSourceAndBaseURL("sk-ant-oat01-initial", tokenSource, server.URL)
|
||||
_, err := provider.Chat(context.Background(), []Message{{Role: "user", Content: "test"}}, nil, "claude-sonnet-4.6", map[string]any{"max_tokens": 100})
|
||||
if err != nil {
|
||||
t.Fatalf("Chat() error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenSource_WithStandardAPIKey_UsesStandardAuth(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Verify standard API key uses Authorization header without beta
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader != "Bearer sk-ant-api03-refreshed" {
|
||||
t.Errorf("Authorization header = %q, want %q", authHeader, "Bearer sk-ant-api03-refreshed")
|
||||
}
|
||||
|
||||
// Should NOT have OAuth beta header
|
||||
if betaHeader := r.Header.Get("anthropic-beta"); betaHeader != "" {
|
||||
t.Errorf("anthropic-beta header should not be set for standard key, got %q", betaHeader)
|
||||
}
|
||||
|
||||
resp := map[string]any{
|
||||
"id": "msg_test",
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"model": "claude-sonnet-4.6",
|
||||
"stop_reason": "end_turn",
|
||||
"content": []map[string]any{
|
||||
{"type": "text", "text": "Refreshed standard auth works"},
|
||||
},
|
||||
"usage": map[string]any{
|
||||
"input_tokens": 5,
|
||||
"output_tokens": 4,
|
||||
},
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tokenSource := func() (string, error) {
|
||||
return "sk-ant-api03-refreshed", nil
|
||||
}
|
||||
|
||||
provider := NewProviderWithTokenSourceAndBaseURL("sk-ant-api03-initial", tokenSource, server.URL)
|
||||
_, err := provider.Chat(context.Background(), []Message{{Role: "user", Content: "test"}}, nil, "claude-sonnet-4.6", map[string]any{"max_tokens": 100})
|
||||
if err != nil {
|
||||
t.Fatalf("Chat() error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOAuthToken_WithWhitespace_TrimmedCorrectly(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
// Should be trimmed
|
||||
if authHeader != "Bearer sk-ant-oat01-test" {
|
||||
t.Errorf("Authorization header = %q, want %q (should be trimmed)", authHeader, "Bearer sk-ant-oat01-test")
|
||||
}
|
||||
|
||||
resp := map[string]any{
|
||||
"id": "msg_test",
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"model": "claude-sonnet-4.6",
|
||||
"stop_reason": "end_turn",
|
||||
"content": []map[string]any{
|
||||
{"type": "text", "text": "ok"},
|
||||
},
|
||||
"usage": map[string]any{
|
||||
"input_tokens": 1,
|
||||
"output_tokens": 1,
|
||||
},
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
provider := NewProviderWithBaseURL(" sk-ant-oat01-test ", server.URL)
|
||||
_, err := provider.Chat(context.Background(), []Message{{Role: "user", Content: "test"}}, nil, "claude-sonnet-4.6", map[string]any{"max_tokens": 100})
|
||||
if err != nil {
|
||||
t.Fatalf("Chat() error: %v", err)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue