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 }