...

Source file src/cloud.google.com/go/auth/credentials/internal/externalaccount/externalaccount_test.go

Documentation: cloud.google.com/go/auth/credentials/internal/externalaccount

     1  // Copyright 2023 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package externalaccount
    16  
    17  import (
    18  	"context"
    19  	"encoding/json"
    20  	"fmt"
    21  	"io"
    22  	"net/http"
    23  	"net/http/httptest"
    24  	"testing"
    25  	"time"
    26  
    27  	"cloud.google.com/go/auth"
    28  	"cloud.google.com/go/auth/credentials/internal/stsexchange"
    29  	"cloud.google.com/go/auth/internal"
    30  	"cloud.google.com/go/auth/internal/credsfile"
    31  )
    32  
    33  const (
    34  	textBaseCredPath                              = "testdata/3pi_cred.txt"
    35  	jsonBaseCredPath                              = "testdata/3pi_cred.json"
    36  	baseCredsRequestBody                          = "audience=32555940559.apps.googleusercontent.com&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdevstorage.full_control&subject_token=street123&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aid_token"
    37  	baseCredsResponseBody                         = `{"access_token":"Sample.Access.Token","issued_token_type":"urn:ietf:params:oauth:token-type:access_token","token_type":"Bearer","expires_in":3600,"scope":"https://www.googleapis.com/auth/cloud-platform"}`
    38  	workforcePoolRequestBodyWithClientID          = "audience=%2F%2Fiam.googleapis.com%2Flocations%2Feu%2FworkforcePools%2Fpool-id%2Fproviders%2Fprovider-id&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdevstorage.full_control&subject_token=street123&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aid_token"
    39  	workforcePoolRequestBodyWithoutClientID       = "audience=%2F%2Fiam.googleapis.com%2Flocations%2Feu%2FworkforcePools%2Fpool-id%2Fproviders%2Fprovider-id&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&options=%7B%22userProject%22%3A%22myProject%22%7D&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdevstorage.full_control&subject_token=street123&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aid_token"
    40  	correctAT                                     = "Sample.Access.Token"
    41  	expiry                                  int64 = 234852
    42  )
    43  
    44  var (
    45  	testOpts = &Options{
    46  		Audience:         "32555940559.apps.googleusercontent.com",
    47  		SubjectTokenType: jwtTokenType,
    48  		TokenInfoURL:     "http://localhost:8080/v1/tokeninfo",
    49  		ClientSecret:     "notsosecret",
    50  		ClientID:         "rbrgnognrhongo3bi4gb9ghg9g",
    51  		CredentialSource: testBaseCredSource,
    52  		Scopes:           []string{"https://www.googleapis.com/auth/devstorage.full_control"},
    53  		Client:           internal.CloneDefaultClient(),
    54  	}
    55  	testBaseCredSource = &credsfile.CredentialSource{
    56  		File:   textBaseCredPath,
    57  		Format: &credsfile.Format{Type: fileTypeText},
    58  	}
    59  	testNow = func() time.Time { return time.Unix(expiry, 0) }
    60  )
    61  
    62  func TestToken(t *testing.T) {
    63  	tests := []struct {
    64  		name      string
    65  		respBody  *stsexchange.TokenResponse
    66  		wantError bool
    67  	}{
    68  		{
    69  			name: "works",
    70  			respBody: &stsexchange.TokenResponse{
    71  				AccessToken:     correctAT,
    72  				IssuedTokenType: "urn:ietf:params:oauth:token-type:access_token",
    73  				TokenType:       "Bearer",
    74  				ExpiresIn:       3600,
    75  				Scope:           "https://www.googleapis.com/auth/cloud-platform",
    76  			},
    77  		},
    78  		{
    79  			name: "no exp time on tok",
    80  			respBody: &stsexchange.TokenResponse{
    81  				AccessToken:     correctAT,
    82  				IssuedTokenType: "urn:ietf:params:oauth:token-type:access_token",
    83  				TokenType:       "Bearer",
    84  				Scope:           "https://www.googleapis.com/auth/cloud-platform",
    85  			},
    86  			wantError: true,
    87  		},
    88  		{
    89  			name: "negative exp time",
    90  			respBody: &stsexchange.TokenResponse{
    91  				AccessToken:     correctAT,
    92  				IssuedTokenType: "urn:ietf:params:oauth:token-type:access_token",
    93  				TokenType:       "Bearer",
    94  				ExpiresIn:       -1,
    95  				Scope:           "https://www.googleapis.com/auth/cloud-platform",
    96  			},
    97  			wantError: true,
    98  		},
    99  	}
   100  	for _, tt := range tests {
   101  		opts := &Options{
   102  			Audience:         "32555940559.apps.googleusercontent.com",
   103  			SubjectTokenType: idTokenType,
   104  			ClientSecret:     "notsosecret",
   105  			ClientID:         "rbrgnognrhongo3bi4gb9ghg9g",
   106  			CredentialSource: testBaseCredSource,
   107  			Scopes:           []string{"https://www.googleapis.com/auth/devstorage.full_control"},
   108  		}
   109  
   110  		respBody, err := json.Marshal(tt.respBody)
   111  		if err != nil {
   112  			t.Fatal(err)
   113  		}
   114  
   115  		server := &testExchangeTokenServer{
   116  			url:           "/",
   117  			authorization: "Basic cmJyZ25vZ25yaG9uZ28zYmk0Z2I5Z2hnOWc6bm90c29zZWNyZXQ=",
   118  			contentType:   "application/x-www-form-urlencoded",
   119  			body:          baseCredsRequestBody,
   120  			response:      string(respBody),
   121  			metricsHeader: expectedMetricsHeader("file", false, false),
   122  		}
   123  
   124  		tok, err := run(t, opts, server)
   125  		if err != nil && !tt.wantError {
   126  			t.Fatal(err)
   127  		}
   128  		if tt.wantError {
   129  			if err == nil {
   130  				t.Fatal("want err, got nil")
   131  			}
   132  			continue
   133  		}
   134  		validateToken(t, tok)
   135  	}
   136  	opts := &Options{
   137  		Audience:         "32555940559.apps.googleusercontent.com",
   138  		SubjectTokenType: idTokenType,
   139  		ClientSecret:     "notsosecret",
   140  		ClientID:         "rbrgnognrhongo3bi4gb9ghg9g",
   141  		CredentialSource: testBaseCredSource,
   142  		Scopes:           []string{"https://www.googleapis.com/auth/devstorage.full_control"},
   143  	}
   144  
   145  	server := &testExchangeTokenServer{
   146  		url:           "/",
   147  		authorization: "Basic cmJyZ25vZ25yaG9uZ28zYmk0Z2I5Z2hnOWc6bm90c29zZWNyZXQ=",
   148  		contentType:   "application/x-www-form-urlencoded",
   149  		body:          baseCredsRequestBody,
   150  		response:      baseCredsResponseBody,
   151  		metricsHeader: expectedMetricsHeader("file", false, false),
   152  	}
   153  
   154  	tok, err := run(t, opts, server)
   155  	if err != nil {
   156  		t.Fatal(err)
   157  	}
   158  	validateToken(t, tok)
   159  }
   160  
   161  func TestWorkforcePoolTokenWithClientID(t *testing.T) {
   162  	opts := Options{
   163  		Audience:                 "//iam.googleapis.com/locations/eu/workforcePools/pool-id/providers/provider-id",
   164  		SubjectTokenType:         idTokenType,
   165  		ClientSecret:             "notsosecret",
   166  		ClientID:                 "rbrgnognrhongo3bi4gb9ghg9g",
   167  		CredentialSource:         testBaseCredSource,
   168  		Scopes:                   []string{"https://www.googleapis.com/auth/devstorage.full_control"},
   169  		WorkforcePoolUserProject: "myProject",
   170  	}
   171  
   172  	server := testExchangeTokenServer{
   173  		url:           "/",
   174  		authorization: "Basic cmJyZ25vZ25yaG9uZ28zYmk0Z2I5Z2hnOWc6bm90c29zZWNyZXQ=",
   175  		contentType:   "application/x-www-form-urlencoded",
   176  		body:          workforcePoolRequestBodyWithClientID,
   177  		response:      baseCredsResponseBody,
   178  		metricsHeader: expectedMetricsHeader("file", false, false),
   179  	}
   180  
   181  	tok, err := run(t, &opts, &server)
   182  	if err != nil {
   183  		t.Fatal(err)
   184  	}
   185  	validateToken(t, tok)
   186  }
   187  
   188  func TestWorkforcePoolTokenWithoutClientID(t *testing.T) {
   189  	opts := Options{
   190  		Audience:                 "//iam.googleapis.com/locations/eu/workforcePools/pool-id/providers/provider-id",
   191  		SubjectTokenType:         idTokenType,
   192  		ClientSecret:             "notsosecret",
   193  		CredentialSource:         testBaseCredSource,
   194  		Scopes:                   []string{"https://www.googleapis.com/auth/devstorage.full_control"},
   195  		WorkforcePoolUserProject: "myProject",
   196  	}
   197  
   198  	server := testExchangeTokenServer{
   199  		url:           "/",
   200  		authorization: "",
   201  		contentType:   "application/x-www-form-urlencoded",
   202  		body:          workforcePoolRequestBodyWithoutClientID,
   203  		response:      baseCredsResponseBody,
   204  		metricsHeader: expectedMetricsHeader("file", false, false),
   205  	}
   206  
   207  	tok, err := run(t, &opts, &server)
   208  	if err != nil {
   209  		t.Fatal(err)
   210  	}
   211  	validateToken(t, tok)
   212  }
   213  
   214  func TestNonworkforceWithWorkforcePoolUserProject(t *testing.T) {
   215  	opts := &Options{
   216  		Audience:                 "32555940559.apps.googleusercontent.com",
   217  		SubjectTokenType:         idTokenType,
   218  		TokenURL:                 "https://sts.googleapis.com",
   219  		ClientSecret:             "notsosecret",
   220  		ClientID:                 "rbrgnognrhongo3bi4gb9ghg9g",
   221  		CredentialSource:         testBaseCredSource,
   222  		Scopes:                   []string{"https://www.googleapis.com/auth/devstorage.full_control"},
   223  		WorkforcePoolUserProject: "myProject",
   224  		Client:                   internal.CloneDefaultClient(),
   225  	}
   226  
   227  	_, err := NewTokenProvider(opts)
   228  	if err == nil {
   229  		t.Fatalf("got nil, want an error")
   230  	}
   231  	if got, want := err.Error(), "externalaccount: workforce_pool_user_project should not be set for non-workforce pool credentials"; got != want {
   232  		t.Errorf("got %v, want %v", got, want)
   233  	}
   234  }
   235  
   236  func TestWorkforcePoolCreation(t *testing.T) {
   237  	var audienceValidatyTests = []struct {
   238  		audience      string
   239  		expectSuccess bool
   240  	}{
   241  		{"//iam.googleapis.com/locations/global/workforcePools/pool-id/providers/provider-id", true},
   242  		{"//iam.googleapis.com/locations/eu/workforcePools/pool-id/providers/provider-id", true},
   243  		{"//iam.googleapis.com/locations/eu/workforcePools/workloadIdentityPools/providers/provider-id", true},
   244  		{"identitynamespace:1f12345:my_provider", false},
   245  		{"//iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/pool-id/providers/provider-id", false},
   246  		{"//iam.googleapis.com/projects/123456/locations/eu/workloadIdentityPools/pool-id/providers/provider-id", false},
   247  		{"//iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/workforcePools/providers/provider-id", false},
   248  		{"//iamgoogleapis.com/locations/eu/workforcePools/pool-id/providers/provider-id", false},
   249  		{"//iam.googleapiscom/locations/eu/workforcePools/pool-id/providers/provider-id", false},
   250  		{"//iam.googleapis.com/locations/workforcePools/pool-id/providers/provider-id", false},
   251  		{"//iam.googleapis.com/locations/eu/workforcePool/pool-id/providers/provider-id", false},
   252  		{"//iam.googleapis.com/locations//workforcePool/pool-id/providers/provider-id", false},
   253  	}
   254  	for _, tt := range audienceValidatyTests {
   255  		t.Run(" "+tt.audience, func(t *testing.T) { // We prepend a space ahead of the test input when outputting for sake of readability.
   256  			opts := testOpts
   257  			opts.TokenURL = "https://sts.googleapis.com" // Setting the most basic acceptable tokenURL
   258  			opts.ServiceAccountImpersonationURL = "https://iamcredentials.googleapis.com"
   259  			opts.Audience = tt.audience
   260  			opts.WorkforcePoolUserProject = "myProject"
   261  			_, err := NewTokenProvider(opts)
   262  
   263  			if tt.expectSuccess && err != nil {
   264  				t.Errorf("got %v, want nil", err)
   265  			} else if !tt.expectSuccess && err == nil {
   266  				t.Errorf("got nil, want an error")
   267  			}
   268  		})
   269  	}
   270  }
   271  
   272  type testExchangeTokenServer struct {
   273  	url           string
   274  	authorization string
   275  	contentType   string
   276  	body          string
   277  	response      string
   278  	metricsHeader string
   279  }
   280  
   281  func run(t *testing.T, opts *Options, tets *testExchangeTokenServer) (*auth.Token, error) {
   282  	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   283  		if got, want := r.URL.String(), tets.url; got != want {
   284  			t.Errorf("got %v, want %v", got, want)
   285  		}
   286  		headerAuth := r.Header.Get("Authorization")
   287  		if got, want := headerAuth, tets.authorization; got != want {
   288  			t.Errorf("got %v, want %v", got, want)
   289  		}
   290  		headerContentType := r.Header.Get("Content-Type")
   291  		if got, want := headerContentType, tets.contentType; got != want {
   292  			t.Errorf("got %v, want %v", got, want)
   293  		}
   294  		headerMetrics := r.Header.Get("x-goog-api-client")
   295  		if got, want := headerMetrics, tets.metricsHeader; got != want {
   296  			t.Errorf("got %v but want %v", got, want)
   297  		}
   298  		body, err := io.ReadAll(r.Body)
   299  		if err != nil {
   300  			t.Fatalf("Failed reading request body: %s.", err)
   301  		}
   302  		if got, want := string(body), tets.body; got != want {
   303  			t.Errorf("got %v, want %v", got, want)
   304  		}
   305  		w.Header().Set("Content-Type", "application/json")
   306  		w.Write([]byte(tets.response))
   307  	}))
   308  	defer server.Close()
   309  	opts.TokenURL = server.URL
   310  
   311  	oldNow := Now
   312  	defer func() { Now = oldNow }()
   313  	Now = testNow
   314  
   315  	stp, err := newSubjectTokenProvider(opts)
   316  	if err != nil {
   317  		t.Fatal(err)
   318  	}
   319  	tp := &tokenProvider{
   320  		opts:   opts,
   321  		client: internal.CloneDefaultClient(),
   322  		stp:    stp,
   323  	}
   324  
   325  	return tp.Token(context.Background())
   326  }
   327  
   328  func validateToken(t *testing.T, tok *auth.Token) {
   329  	if got, want := tok.Value, correctAT; got != want {
   330  		t.Errorf("got %v, want %v", got, want)
   331  	}
   332  	if got, want := tok.Type, internal.TokenTypeBearer; got != want {
   333  		t.Errorf("got %v, want %v", got, want)
   334  	}
   335  
   336  	if got, want := tok.Expiry, testNow().Add(time.Duration(3600)*time.Second); got != want {
   337  		t.Errorf("got %v, want %v", got, want)
   338  	}
   339  }
   340  
   341  func cloneTestOpts() *Options {
   342  	return &Options{
   343  		Audience:                       "32555940559.apps.googleusercontent.com",
   344  		SubjectTokenType:               jwtTokenType,
   345  		TokenURL:                       "http://localhost:8080/v1/token",
   346  		TokenInfoURL:                   "http://localhost:8080/v1/tokeninfo",
   347  		ServiceAccountImpersonationURL: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/service-gcs-admin@$PROJECT_ID.iam.gserviceaccount.com:generateAccessToken",
   348  		ClientSecret:                   "notsosecret",
   349  		ClientID:                       "rbrgnognrhongo3bi4gb9ghg9g",
   350  		Client:                         internal.CloneDefaultClient(),
   351  	}
   352  }
   353  
   354  func expectedMetricsHeader(source string, saImpersonation bool, configLifetime bool) string {
   355  	return fmt.Sprintf("gl-go/%s auth/unknown google-byoid-sdk source/%s sa-impersonation/%t config-lifetime/%t", goVersion(), source, saImpersonation, configLifetime)
   356  }
   357  
   358  func TestOptionsValidate(t *testing.T) {
   359  	tests := []struct {
   360  		name    string
   361  		o       *Options
   362  		wantErr bool
   363  	}{
   364  		{
   365  			name: "works",
   366  			o: &Options{
   367  				Audience:                       "32555940559.apps.googleusercontent.com",
   368  				SubjectTokenType:               jwtTokenType,
   369  				TokenURL:                       "http://localhost:8080/v1/token",
   370  				TokenInfoURL:                   "http://localhost:8080/v1/tokeninfo",
   371  				ServiceAccountImpersonationURL: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/service-gcs-admin@$PROJECT_ID.iam.gserviceaccount.com:generateAccessToken",
   372  				ClientSecret:                   "notsosecret",
   373  				ClientID:                       "rbrgnognrhongo3bi4gb9ghg9g",
   374  				Client:                         internal.CloneDefaultClient(),
   375  				CredentialSource:               testBaseCredSource,
   376  			},
   377  		},
   378  		{
   379  			name: "missing aud",
   380  			o: &Options{
   381  				SubjectTokenType:               jwtTokenType,
   382  				TokenURL:                       "http://localhost:8080/v1/token",
   383  				TokenInfoURL:                   "http://localhost:8080/v1/tokeninfo",
   384  				ServiceAccountImpersonationURL: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/service-gcs-admin@$PROJECT_ID.iam.gserviceaccount.com:generateAccessToken",
   385  				ClientSecret:                   "notsosecret",
   386  				ClientID:                       "rbrgnognrhongo3bi4gb9ghg9g",
   387  				Client:                         internal.CloneDefaultClient(),
   388  				CredentialSource:               testBaseCredSource,
   389  			},
   390  			wantErr: true,
   391  		},
   392  		{
   393  			name: "missing subjectTokenType",
   394  			o: &Options{
   395  				Audience:                       "32555940559.apps.googleusercontent.com",
   396  				TokenURL:                       "http://localhost:8080/v1/token",
   397  				TokenInfoURL:                   "http://localhost:8080/v1/tokeninfo",
   398  				ServiceAccountImpersonationURL: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/service-gcs-admin@$PROJECT_ID.iam.gserviceaccount.com:generateAccessToken",
   399  				ClientSecret:                   "notsosecret",
   400  				ClientID:                       "rbrgnognrhongo3bi4gb9ghg9g",
   401  				Client:                         internal.CloneDefaultClient(),
   402  				CredentialSource:               testBaseCredSource,
   403  			},
   404  			wantErr: true,
   405  		},
   406  		{
   407  			name: "invalid workforcepool",
   408  			o: &Options{
   409  				WorkforcePoolUserProject:       "blah",
   410  				Audience:                       "32555940559.apps.googleusercontent.com",
   411  				SubjectTokenType:               jwtTokenType,
   412  				TokenURL:                       "http://localhost:8080/v1/token",
   413  				TokenInfoURL:                   "http://localhost:8080/v1/tokeninfo",
   414  				ServiceAccountImpersonationURL: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/service-gcs-admin@$PROJECT_ID.iam.gserviceaccount.com:generateAccessToken",
   415  				ClientSecret:                   "notsosecret",
   416  				ClientID:                       "rbrgnognrhongo3bi4gb9ghg9g",
   417  				Client:                         internal.CloneDefaultClient(),
   418  				CredentialSource:               testBaseCredSource,
   419  			},
   420  			wantErr: true,
   421  		},
   422  		{
   423  			name: "no creds",
   424  			o: &Options{
   425  				Audience:                       "32555940559.apps.googleusercontent.com",
   426  				SubjectTokenType:               jwtTokenType,
   427  				TokenURL:                       "http://localhost:8080/v1/token",
   428  				TokenInfoURL:                   "http://localhost:8080/v1/tokeninfo",
   429  				ServiceAccountImpersonationURL: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/service-gcs-admin@$PROJECT_ID.iam.gserviceaccount.com:generateAccessToken",
   430  				ClientSecret:                   "notsosecret",
   431  				ClientID:                       "rbrgnognrhongo3bi4gb9ghg9g",
   432  				Client:                         internal.CloneDefaultClient(),
   433  			},
   434  			wantErr: true,
   435  		},
   436  		{
   437  			name: "too many creds",
   438  			o: &Options{
   439  				Audience:                       "32555940559.apps.googleusercontent.com",
   440  				SubjectTokenType:               jwtTokenType,
   441  				TokenURL:                       "http://localhost:8080/v1/token",
   442  				TokenInfoURL:                   "http://localhost:8080/v1/tokeninfo",
   443  				ServiceAccountImpersonationURL: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/service-gcs-admin@$PROJECT_ID.iam.gserviceaccount.com:generateAccessToken",
   444  				ClientSecret:                   "notsosecret",
   445  				ClientID:                       "rbrgnognrhongo3bi4gb9ghg9g",
   446  				Client:                         internal.CloneDefaultClient(),
   447  				CredentialSource:               testBaseCredSource,
   448  				SubjectTokenProvider:           fakeSubjectTokenProvider{},
   449  			},
   450  			wantErr: true,
   451  		},
   452  	}
   453  	for _, tc := range tests {
   454  		t.Run(tc.name, func(t *testing.T) {
   455  			err := tc.o.validate()
   456  			if err == nil && tc.wantErr {
   457  				t.Fatalf("o.validate() = nil, want error")
   458  			}
   459  			if err != nil && !tc.wantErr {
   460  				t.Fatalf("o.validate() = non-nil error, want error")
   461  			}
   462  		})
   463  	}
   464  }
   465  
   466  func TestOptionsResolveTokenURL(t *testing.T) {
   467  	tests := []struct {
   468  		name string
   469  		o    *Options
   470  		want string
   471  	}{
   472  		{
   473  			name: "default",
   474  			o:    &Options{},
   475  			want: "https://sts.googleapis.com/v1/token",
   476  		},
   477  		{
   478  			name: "Options TokenURL",
   479  			o: &Options{
   480  				TokenURL: "http://localhost:8080/v1/token",
   481  			},
   482  			want: "http://localhost:8080/v1/token",
   483  		},
   484  		{
   485  			name: "Options UniverseDomain",
   486  			o: &Options{
   487  				UniverseDomain: "example.com",
   488  			},
   489  			want: "https://sts.example.com/v1/token",
   490  		},
   491  		{
   492  			name: "Options TokenURL overrides UniverseDomain",
   493  			o: &Options{
   494  				TokenURL:       "http://localhost:8080/v1/token",
   495  				UniverseDomain: "example.com",
   496  			},
   497  			want: "http://localhost:8080/v1/token",
   498  		},
   499  	}
   500  	for _, tc := range tests {
   501  		t.Run(tc.name, func(t *testing.T) {
   502  			tc.o.resolveTokenURL()
   503  			if tc.o.TokenURL != tc.want {
   504  				t.Errorf("got %s, want %s", tc.o.TokenURL, tc.want)
   505  			}
   506  		})
   507  	}
   508  }
   509  

View as plain text