github.com/anthropics/anthropic-sdk-go@v1.70.1/internal/auth/default_test.go (about) 1 package auth 2 3 import ( 4 "context" 5 "encoding/json" 6 "net/http" 7 "net/http/httptest" 8 "os" 9 "path/filepath" 10 "testing" 11 12 "github.com/anthropics/anthropic-sdk-go/config" 13 ) 14 15 // unsetEnv removes an environment variable for the duration of a test. 16 // t.Setenv registers the original value for automatic restoration at test 17 // end; os.Unsetenv then actually removes the var for this test. 18 func unsetEnv(t *testing.T, key string) { 19 t.Helper() 20 t.Setenv(key, "") 21 os.Unsetenv(key) 22 } 23 24 // userOAuthProfileBody returns a minimal user_oauth config map suitable for 25 // the profile-system tests. 26 func userOAuthProfileBody() map[string]any { 27 return map[string]any{ 28 "authentication": map[string]any{ 29 "type": "user_oauth", 30 }, 31 } 32 } 33 34 // setupProfile creates a config dir with configs/<name>.json and optionally 35 // credentials/<name>.json, returning the config dir path. 36 func setupProfile(t *testing.T, name string, cfgData map[string]any, creds map[string]any) string { 37 t.Helper() 38 dir := t.TempDir() 39 configsDir := filepath.Join(dir, "configs") 40 credsDir := filepath.Join(dir, "credentials") 41 os.MkdirAll(configsDir, 0755) 42 os.MkdirAll(credsDir, 0700) 43 44 b, _ := json.MarshalIndent(cfgData, "", " ") 45 os.WriteFile(filepath.Join(configsDir, name+".json"), b, 0644) 46 47 if creds != nil { 48 b, _ = json.MarshalIndent(creds, "", " ") 49 os.WriteFile(filepath.Join(credsDir, name+".json"), b, 0600) 50 } 51 return dir 52 } 53 54 func TestLoadConfig_ProfileSystem(t *testing.T) { 55 dir := setupProfile(t, "default", userOAuthProfileBody(), map[string]any{ 56 "type": "oauth_token", 57 "access_token": "profile-tok", 58 }) 59 t.Setenv("ANTHROPIC_CONFIG_DIR", dir) 60 61 cfg, err := config.LoadConfig() 62 if err != nil { 63 t.Fatal(err) 64 } 65 result, err := ResolveCredentials(cfg) 66 if err != nil { 67 t.Fatal(err) 68 } 69 tok, err := result.Provider(context.Background(), "", nil) 70 if err != nil { 71 t.Fatal(err) 72 } 73 if tok.Token != "profile-tok" { 74 t.Fatalf("got %q, want %q", tok.Token, "profile-tok") 75 } 76 } 77 78 func TestLoadConfig_ProfileEnv(t *testing.T) { 79 dir := setupProfile(t, "work", userOAuthProfileBody(), map[string]any{ 80 "type": "oauth_token", 81 "access_token": "work-tok", 82 }) 83 t.Setenv("ANTHROPIC_CONFIG_DIR", dir) 84 t.Setenv("ANTHROPIC_PROFILE", "work") 85 86 cfg, err := config.LoadConfig() 87 if err != nil { 88 t.Fatal(err) 89 } 90 result, err := ResolveCredentials(cfg) 91 if err != nil { 92 t.Fatal(err) 93 } 94 tok, err := result.Provider(context.Background(), "", nil) 95 if err != nil { 96 t.Fatal(err) 97 } 98 if tok.Token != "work-tok" { 99 t.Fatalf("got %q, want %q", tok.Token, "work-tok") 100 } 101 } 102 103 func TestLoadConfig_ActiveConfigFile(t *testing.T) { 104 dir := setupProfile(t, "staging", userOAuthProfileBody(), map[string]any{ 105 "type": "oauth_token", 106 "access_token": "staging-tok", 107 }) 108 os.WriteFile(filepath.Join(dir, "active_config"), []byte("staging"), 0644) 109 t.Setenv("ANTHROPIC_CONFIG_DIR", dir) 110 111 cfg, err := config.LoadConfig() 112 if err != nil { 113 t.Fatal(err) 114 } 115 result, err := ResolveCredentials(cfg) 116 if err != nil { 117 t.Fatal(err) 118 } 119 tok, err := result.Provider(context.Background(), "", nil) 120 if err != nil { 121 t.Fatal(err) 122 } 123 if tok.Token != "staging-tok" { 124 t.Fatalf("got %q, want %q", tok.Token, "staging-tok") 125 } 126 } 127 128 func TestLoadConfig_NothingSetReturnsError(t *testing.T) { 129 dir := t.TempDir() 130 t.Setenv("ANTHROPIC_CONFIG_DIR", dir) 131 132 _, err := config.LoadConfig() 133 if err == nil { 134 t.Fatal("expected error when no config file exists") 135 } 136 } 137 138 // TestLoadConfig_EnvFillsMissingWorkspaceID verifies that ANTHROPIC_WORKSPACE_ID 139 // fills the top-level workspace_id config key when the profile omits it, and 140 // that the fill propagates through ResolveCredentials into the jwt-bearer 141 // exchange body. 142 func TestLoadConfig_EnvFillsMissingWorkspaceID(t *testing.T) { 143 var receivedBody tokenExchangeRequest 144 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 145 if err := json.NewDecoder(r.Body).Decode(&receivedBody); err != nil { 146 t.Fatal(err) 147 } 148 json.NewEncoder(w).Encode(tokenExchangeResponse{AccessToken: "tok"}) 149 })) 150 defer server.Close() 151 152 tokenDir := t.TempDir() 153 tokenPath := filepath.Join(tokenDir, "tok") 154 os.WriteFile(tokenPath, []byte("jwt"), 0600) 155 156 // Profile has oidc_federation auth with no workspace_id at any level. 157 dir := setupProfile(t, "default", map[string]any{ 158 "organization_id": "org_x", 159 "authentication": map[string]any{ 160 "type": "oidc_federation", 161 "federation_rule_id": "fdrl_01abc", 162 }, 163 }, nil) 164 t.Setenv("ANTHROPIC_CONFIG_DIR", dir) 165 t.Setenv(EnvIdentityTokenFile, tokenPath) 166 t.Setenv(EnvWorkspaceID, "wrkspc_from_env") 167 168 cfg, err := config.LoadConfig() 169 if err != nil { 170 t.Fatal(err) 171 } 172 if cfg.WorkspaceID != "wrkspc_from_env" { 173 t.Fatalf("got workspace_id %q, want env fill-in", cfg.WorkspaceID) 174 } 175 result, err := ResolveCredentials(cfg) 176 if err != nil { 177 t.Fatal(err) 178 } 179 if _, err := result.Provider(context.Background(), server.URL, http.DefaultClient.Do); err != nil { 180 t.Fatal(err) 181 } 182 if receivedBody.WorkspaceID != "wrkspc_from_env" { 183 t.Fatalf("got exchange-body workspace_id %q, want %q", receivedBody.WorkspaceID, "wrkspc_from_env") 184 } 185 } 186 187 func TestEnvCredentials_WorkloadIdentity(t *testing.T) { 188 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 189 json.NewEncoder(w).Encode(tokenExchangeResponse{AccessToken: "wi-tok"}) 190 })) 191 defer server.Close() 192 193 dir := t.TempDir() 194 tokenPath := filepath.Join(dir, "token") 195 os.WriteFile(tokenPath, []byte("my-jwt"), 0600) 196 197 // Set config dir to empty dir so no profile config exists. 198 t.Setenv("ANTHROPIC_CONFIG_DIR", dir) 199 t.Setenv(EnvIdentityTokenFile, tokenPath) 200 t.Setenv(EnvFederationRuleID, "rule-1") 201 t.Setenv(EnvOrganizationID, "org-1") 202 203 result, _, _ := EnvCredentials() 204 if result == nil { 205 t.Fatal("expected non-nil") 206 } 207 tok, err := result.Provider(context.Background(), server.URL, http.DefaultClient.Do) 208 if err != nil { 209 t.Fatal(err) 210 } 211 if tok.Token != "wi-tok" { 212 t.Fatalf("got %q, want %q", tok.Token, "wi-tok") 213 } 214 } 215 216 func TestEnvCredentials_LiteralToken(t *testing.T) { 217 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 218 json.NewEncoder(w).Encode(tokenExchangeResponse{AccessToken: "wi-tok"}) 219 })) 220 defer server.Close() 221 222 dir := t.TempDir() 223 t.Setenv("ANTHROPIC_CONFIG_DIR", dir) 224 t.Setenv(EnvIdentityToken, "literal-jwt") 225 t.Setenv(EnvFederationRuleID, "rule-1") 226 t.Setenv(EnvOrganizationID, "org-1") 227 228 result, _, _ := EnvCredentials() 229 if result == nil { 230 t.Fatal("expected non-nil") 231 } 232 tok, err := result.Provider(context.Background(), server.URL, http.DefaultClient.Do) 233 if err != nil { 234 t.Fatal(err) 235 } 236 if tok.Token != "wi-tok" { 237 t.Fatalf("got %q, want %q", tok.Token, "wi-tok") 238 } 239 } 240 241 // TestEnvCredentials_WorkspaceID verifies that ANTHROPIC_WORKSPACE_ID is 242 // read in the env-var federation chain and forwarded as workspace_id in 243 // the jwt-bearer exchange body. 244 func TestEnvCredentials_WorkspaceID(t *testing.T) { 245 var receivedBody tokenExchangeRequest 246 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 247 if err := json.NewDecoder(r.Body).Decode(&receivedBody); err != nil { 248 t.Fatal(err) 249 } 250 json.NewEncoder(w).Encode(tokenExchangeResponse{AccessToken: "wi-tok"}) 251 })) 252 defer server.Close() 253 254 dir := t.TempDir() 255 t.Setenv("ANTHROPIC_CONFIG_DIR", dir) 256 t.Setenv(EnvIdentityToken, "literal-jwt") 257 t.Setenv(EnvFederationRuleID, "fdrl_01abc") 258 t.Setenv(EnvOrganizationID, "org-uuid") 259 t.Setenv(EnvWorkspaceID, "wrkspc_01abc") 260 261 result, _, _ := EnvCredentials() 262 if result == nil { 263 t.Fatal("expected non-nil") 264 } 265 if _, err := result.Provider(context.Background(), server.URL, http.DefaultClient.Do); err != nil { 266 t.Fatal(err) 267 } 268 if receivedBody.WorkspaceID != "wrkspc_01abc" { 269 t.Fatalf("got workspace_id %q, want %q", receivedBody.WorkspaceID, "wrkspc_01abc") 270 } 271 } 272 273 // TestEnvCredentials_WorkspaceIDEmptyTreatedUnset pins the empty-string 274 // behavior: ANTHROPIC_WORKSPACE_ID="" (a defaulted-but-empty CI variable) 275 // must be treated as unset — never put `"workspace_id": ""` on the wire. 276 // In Go this falls out of os.Getenv returning "" plus the 277 // json:"workspace_id,omitempty" tag on tokenExchangeRequest, but the test 278 // exists as a regression pin so neither side of that pair regresses 279 // silently. 280 func TestEnvCredentials_WorkspaceIDEmptyTreatedUnset(t *testing.T) { 281 var receivedRaw map[string]any 282 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 283 if err := json.NewDecoder(r.Body).Decode(&receivedRaw); err != nil { 284 t.Fatal(err) 285 } 286 json.NewEncoder(w).Encode(tokenExchangeResponse{AccessToken: "wi-tok"}) 287 })) 288 defer server.Close() 289 290 dir := t.TempDir() 291 t.Setenv("ANTHROPIC_CONFIG_DIR", dir) 292 t.Setenv(EnvIdentityToken, "literal-jwt") 293 t.Setenv(EnvFederationRuleID, "fdrl_01abc") 294 t.Setenv(EnvOrganizationID, "org-uuid") 295 t.Setenv(EnvWorkspaceID, "") // explicitly set, but empty 296 297 result, _, _ := EnvCredentials() 298 if result == nil { 299 t.Fatal("expected non-nil") 300 } 301 if _, err := result.Provider(context.Background(), server.URL, http.DefaultClient.Do); err != nil { 302 t.Fatal(err) 303 } 304 if _, present := receivedRaw["workspace_id"]; present { 305 t.Fatalf("workspace_id must be omitted when ANTHROPIC_WORKSPACE_ID is empty, body=%v", receivedRaw) 306 } 307 } 308 309 func TestEnvCredentials_RequiresAllThree(t *testing.T) { 310 dir := t.TempDir() 311 t.Setenv("ANTHROPIC_CONFIG_DIR", dir) 312 // Only federation rule, missing org ID and token. 313 t.Setenv(EnvFederationRuleID, "rule-1") 314 315 result, _, _ := EnvCredentials() 316 if result != nil { 317 t.Fatal("expected nil when org ID and identity token missing") 318 } 319 } 320 321 func TestEnvCredentials_NothingSetReturnsNil(t *testing.T) { 322 dir := t.TempDir() 323 t.Setenv("ANTHROPIC_CONFIG_DIR", dir) 324 unsetEnv(t, EnvIdentityTokenFile) 325 unsetEnv(t, EnvIdentityToken) 326 unsetEnv(t, EnvFederationRuleID) 327 unsetEnv(t, EnvOrganizationID) 328 unsetEnv(t, EnvWorkspaceID) 329 330 result, _, _ := EnvCredentials() 331 if result != nil { 332 t.Fatal("expected nil when nothing is set") 333 } 334 }