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  }