github.com/anthropics/anthropic-sdk-go@v1.70.1/option/requestoption_test.go (about)

     1  package option_test
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"os"
     7  	"path/filepath"
     8  	"strings"
     9  	"testing"
    10  
    11  	"github.com/anthropics/anthropic-sdk-go/internal/requestconfig"
    12  	"github.com/anthropics/anthropic-sdk-go/option"
    13  )
    14  
    15  func TestIdentityTokenFile_ReadsAndTrims(t *testing.T) {
    16  	dir := t.TempDir()
    17  	path := filepath.Join(dir, "token")
    18  	if err := os.WriteFile(path, []byte("  the-jwt\n"), 0o600); err != nil {
    19  		t.Fatal(err)
    20  	}
    21  
    22  	tok, err := option.IdentityTokenFile(path)(context.Background())
    23  	if err != nil {
    24  		t.Fatalf("unexpected error: %v", err)
    25  	}
    26  	if tok != "the-jwt" {
    27  		t.Errorf("got %q, want %q", tok, "the-jwt")
    28  	}
    29  }
    30  
    31  func TestIdentityTokenFile_RereadsOnEachCall(t *testing.T) {
    32  	dir := t.TempDir()
    33  	path := filepath.Join(dir, "token")
    34  	if err := os.WriteFile(path, []byte("v1"), 0o600); err != nil {
    35  		t.Fatal(err)
    36  	}
    37  	fn := option.IdentityTokenFile(path)
    38  
    39  	got1, err := fn(context.Background())
    40  	if err != nil {
    41  		t.Fatal(err)
    42  	}
    43  	if got1 != "v1" {
    44  		t.Errorf("first call got %q, want %q", got1, "v1")
    45  	}
    46  
    47  	// Simulate token rotation.
    48  	if err := os.WriteFile(path, []byte("v2"), 0o600); err != nil {
    49  		t.Fatal(err)
    50  	}
    51  	got2, err := fn(context.Background())
    52  	if err != nil {
    53  		t.Fatal(err)
    54  	}
    55  	if got2 != "v2" {
    56  		t.Errorf("after rotation got %q, want %q (function did not re-read)", got2, "v2")
    57  	}
    58  }
    59  
    60  func TestIdentityTokenFile_Errors(t *testing.T) {
    61  	ctx := context.Background()
    62  
    63  	t.Run("empty path", func(t *testing.T) {
    64  		if _, err := option.IdentityTokenFile("")(ctx); err == nil || !strings.Contains(err.Error(), "path is empty") {
    65  			t.Errorf("got %v, want error containing 'path is empty'", err)
    66  		}
    67  	})
    68  
    69  	t.Run("missing file", func(t *testing.T) {
    70  		missing := filepath.Join(t.TempDir(), "does-not-exist")
    71  		if _, err := option.IdentityTokenFile(missing)(ctx); err == nil || !strings.Contains(err.Error(), "read") {
    72  			t.Errorf("got %v, want read error", err)
    73  		}
    74  	})
    75  
    76  	t.Run("whitespace only", func(t *testing.T) {
    77  		empty := filepath.Join(t.TempDir(), "empty")
    78  		if err := os.WriteFile(empty, []byte("   \n\t\n"), 0o600); err != nil {
    79  			t.Fatal(err)
    80  		}
    81  		if _, err := option.IdentityTokenFile(empty)(ctx); err == nil || !strings.Contains(err.Error(), "is empty") {
    82  			t.Errorf("got %v, want 'is empty' error", err)
    83  		}
    84  	})
    85  }
    86  
    87  // TestJoin_AppliesInOrder verifies that a joined option is equivalent to
    88  // passing its members individually at the same position: members apply in
    89  // order (a later WithMaxRetries wins) and the first error stops the join.
    90  func TestJoin_AppliesInOrder(t *testing.T) {
    91  	cfg := &requestconfig.RequestConfig{}
    92  	joined := option.Join(
    93  		option.WithMaxRetries(1),
    94  		option.WithBaseURL("https://joined.example/"),
    95  		option.WithMaxRetries(7),
    96  	)
    97  	if err := joined.Apply(cfg); err != nil {
    98  		t.Fatalf("unexpected error: %v", err)
    99  	}
   100  	if cfg.MaxRetries != 7 {
   101  		t.Errorf("got MaxRetries %d, want the later member (7) to win", cfg.MaxRetries)
   102  	}
   103  	if got := cfg.BaseURL.String(); got != "https://joined.example/" {
   104  		t.Errorf("got base URL %q, want %q", got, "https://joined.example/")
   105  	}
   106  
   107  	boom := errors.New("boom")
   108  	applied := false
   109  	failing := option.Join(
   110  		requestconfig.RequestOptionFunc(func(*requestconfig.RequestConfig) error { return boom }),
   111  		requestconfig.RequestOptionFunc(func(*requestconfig.RequestConfig) error { applied = true; return nil }),
   112  	)
   113  	if err := failing.Apply(&requestconfig.RequestConfig{}); !errors.Is(err, boom) {
   114  		t.Fatalf("got error %v, want the member's error", err)
   115  	}
   116  	if applied {
   117  		t.Error("members after a failing member must not be applied")
   118  	}
   119  }
   120  
   121  // TestHasWithoutEnvironmentDefaults_SeesThroughJoin pins the contract that
   122  // platform option constructors rely on: a marker returned inside a Join
   123  // (bedrock/vertex bundle it with their own options) must still be detected
   124  // by anthropic.NewClient, however deeply the joins nest — while a Join with
   125  // no marker must not be mistaken for one.
   126  func TestHasWithoutEnvironmentDefaults_SeesThroughJoin(t *testing.T) {
   127  	noop := option.WithHeader("x-noop", "1")
   128  	testCases := []struct {
   129  		name string
   130  		opts []option.RequestOption
   131  		want bool
   132  	}{
   133  		{
   134  			name: "no options",
   135  			opts: nil,
   136  			want: false,
   137  		},
   138  		{
   139  			name: "unrelated options only",
   140  			opts: []option.RequestOption{noop, option.WithAPIKey("k")},
   141  			want: false,
   142  		},
   143  		{
   144  			name: "marker at the top level",
   145  			opts: []option.RequestOption{noop, option.WithoutEnvironmentDefaults()},
   146  			want: true,
   147  		},
   148  		{
   149  			name: "marker nested one join deep",
   150  			opts: []option.RequestOption{noop, option.Join(noop, option.WithoutEnvironmentDefaults(), noop)},
   151  			want: true,
   152  		},
   153  		{
   154  			name: "marker nested two joins deep",
   155  			opts: []option.RequestOption{option.Join(noop, option.Join(noop, option.WithoutEnvironmentDefaults()))},
   156  			want: true,
   157  		},
   158  		{
   159  			name: "joins without a marker",
   160  			opts: []option.RequestOption{option.Join(noop, option.Join(noop, option.WithAPIKey("k"))), option.Join()},
   161  			want: false,
   162  		},
   163  	}
   164  
   165  	for _, tc := range testCases {
   166  		t.Run(tc.name, func(t *testing.T) {
   167  			if got := option.HasWithoutEnvironmentDefaults(tc.opts); got != tc.want {
   168  				t.Errorf("HasWithoutEnvironmentDefaults() = %v, want %v", got, tc.want)
   169  			}
   170  		})
   171  	}
   172  }