From b11f27cf69e341fc10d795ab35958d3dc6cb3965 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Thu, 26 Jun 2025 12:34:13 +0000 Subject: [PATCH 1/3] Remove testify --- credentials/u2m/persistent_auth_test.go | 249 ++++++++++++++++-------- 1 file changed, 164 insertions(+), 85 deletions(-) diff --git a/credentials/u2m/persistent_auth_test.go b/credentials/u2m/persistent_auth_test.go index 5aac4ecda..402ecdecc 100644 --- a/credentials/u2m/persistent_auth_test.go +++ b/credentials/u2m/persistent_auth_test.go @@ -1,4 +1,4 @@ -package u2m_test +package u2m import ( "context" @@ -6,13 +6,11 @@ import ( "fmt" "net/http" "net/url" + "strings" "testing" "time" - "github.com/databricks/databricks-sdk-go/credentials/u2m" "github.com/databricks/databricks-sdk-go/httpclient/fixtures" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "golang.org/x/oauth2" ) @@ -22,51 +20,59 @@ type tokenCacheMock struct { } func (m *tokenCacheMock) Store(key string, t *oauth2.Token) error { - if m.store == nil { - panic("no store mock") - } return m.store(key, t) } func (m *tokenCacheMock) Lookup(key string) (*oauth2.Token, error) { - if m.lookup == nil { - panic("no lookup mock") - } return m.lookup(key) } func TestToken(t *testing.T) { cache := &tokenCacheMock{ lookup: func(key string) (*oauth2.Token, error) { - assert.Equal(t, "https://abc/oidc/accounts/xyz", key) + if key != "https://abc/oidc/accounts/xyz" { + t.Fatalf("lookup(): want key 'https://abc/oidc/accounts/xyz', got %s", key) + } return &oauth2.Token{ AccessToken: "bcd", Expiry: time.Now().Add(1 * time.Minute), }, nil }, } - arg, err := u2m.NewBasicAccountOAuthArgument("https://abc", "xyz") - assert.NoError(t, err) - p, err := u2m.NewPersistentAuth(context.Background(), u2m.WithTokenCache(cache), u2m.WithOAuthArgument(arg)) - require.NoError(t, err) + arg, err := NewBasicAccountOAuthArgument("https://abc", "xyz") + if err != nil { + t.Fatalf("NewBasicAccountOAuthArgument(): want no error, got %v", err) + } + p, err := NewPersistentAuth(context.Background(), WithTokenCache(cache), WithOAuthArgument(arg)) + if err != nil { + t.Fatalf("NewPersistentAuth(): want no error, got %v", err) + } defer p.Close() + tok, err := p.Token() - assert.NoError(t, err) - assert.Equal(t, "bcd", tok.AccessToken) - assert.Equal(t, "", tok.RefreshToken) + + if err != nil { + t.Fatalf("p.Token(): want no error, got %v", err) + } + if tok.AccessToken != "bcd" { + t.Errorf("p.Token(): want access token 'bcd', got %s", tok.AccessToken) + } + if tok.RefreshToken != "" { + t.Errorf("p.Token(): want refresh token '', got %s", tok.RefreshToken) + } } type MockOAuthEndpointSupplier struct{} -func (m MockOAuthEndpointSupplier) GetAccountOAuthEndpoints(ctx context.Context, accountHost string, accountId string) (*u2m.OAuthAuthorizationServer, error) { - return &u2m.OAuthAuthorizationServer{ +func (m MockOAuthEndpointSupplier) GetAccountOAuthEndpoints(ctx context.Context, accountHost string, accountId string) (*OAuthAuthorizationServer, error) { + return &OAuthAuthorizationServer{ AuthorizationEndpoint: fmt.Sprintf("%s/oidc/accounts/%s/v1/authorize", accountHost, accountId), TokenEndpoint: fmt.Sprintf("%s/oidc/accounts/%s/v1/token", accountHost, accountId), }, nil } -func (m MockOAuthEndpointSupplier) GetWorkspaceOAuthEndpoints(ctx context.Context, workspaceHost string) (*u2m.OAuthAuthorizationServer, error) { - return &u2m.OAuthAuthorizationServer{ +func (m MockOAuthEndpointSupplier) GetWorkspaceOAuthEndpoints(ctx context.Context, workspaceHost string) (*OAuthAuthorizationServer, error) { + return &OAuthAuthorizationServer{ AuthorizationEndpoint: fmt.Sprintf("%s/oidc/v1/authorize", workspaceHost), TokenEndpoint: fmt.Sprintf("%s/oidc/v1/token", workspaceHost), }, nil @@ -77,7 +83,9 @@ func TestToken_RefreshesExpiredAccessToken(t *testing.T) { expectedKey := "https://accounts.cloud.databricks.com/oidc/accounts/xyz" cache := &tokenCacheMock{ lookup: func(key string) (*oauth2.Token, error) { - assert.Equal(t, expectedKey, key) + if key != expectedKey { + t.Fatalf("lookup(): want key %s, got %s", expectedKey, key) + } return &oauth2.Token{ AccessToken: "expired", RefreshToken: "cde", @@ -85,17 +93,23 @@ func TestToken_RefreshesExpiredAccessToken(t *testing.T) { }, nil }, store: func(key string, tok *oauth2.Token) error { - assert.Equal(t, expectedKey, key) - assert.Equal(t, "def", tok.RefreshToken) + if key != expectedKey { + t.Fatalf("store(): want key %s, got %s", expectedKey, key) + } + if tok.RefreshToken != "def" { + t.Fatalf("store(): want refresh token 'def', got %s", tok.RefreshToken) + } return nil }, } - arg, err := u2m.NewBasicAccountOAuthArgument("https://accounts.cloud.databricks.com", "xyz") - assert.NoError(t, err) - p, err := u2m.NewPersistentAuth( + arg, err := NewBasicAccountOAuthArgument("https://accounts.cloud.databricks.com", "xyz") + if err != nil { + t.Fatalf("NewBasicAccountOAuthArgument(): want no error, got %v", err) + } + p, err := NewPersistentAuth( ctx, - u2m.WithTokenCache(cache), - u2m.WithHttpClient(&http.Client{ + WithTokenCache(cache), + WithHttpClient(&http.Client{ Transport: fixtures.SliceTransport{ { Method: "POST", @@ -107,22 +121,33 @@ func TestToken_RefreshesExpiredAccessToken(t *testing.T) { }, }, }), - u2m.WithOAuthEndpointSupplier(MockOAuthEndpointSupplier{}), - u2m.WithOAuthArgument(arg), + WithOAuthEndpointSupplier(MockOAuthEndpointSupplier{}), + WithOAuthArgument(arg), ) - require.NoError(t, err) + if err != nil { + t.Errorf("NewPersistentAuth(): want no error, got %v", err) + } defer p.Close() + tok, err := p.Token() - assert.NoError(t, err) - assert.Equal(t, "refreshed", tok.AccessToken) - assert.Equal(t, "", tok.RefreshToken) + if err != nil { + t.Fatalf("p.Token(): want no error, got %v", err) + } + if tok.AccessToken != "refreshed" { + t.Errorf("p.Token(): want access token 'refreshed', got %s", tok.AccessToken) + } + if tok.RefreshToken != "" { + t.Errorf("p.Token(): want refresh token '', got %s", tok.RefreshToken) + } } func TestToken_ReturnsError(t *testing.T) { ctx := context.Background() cache := &tokenCacheMock{ lookup: func(key string) (*oauth2.Token, error) { - assert.Equal(t, "https://accounts.cloud.databricks.com/oidc/accounts/xyz", key) + if key != "https://accounts.cloud.databricks.com/oidc/accounts/xyz" { + t.Fatalf("lookup(): want key 'https://accounts.cloud.databricks.com/oidc/accounts/xyz', got %s", key) + } return &oauth2.Token{ AccessToken: "expired", RefreshToken: "cde", @@ -130,12 +155,14 @@ func TestToken_ReturnsError(t *testing.T) { }, nil }, } - arg, err := u2m.NewBasicAccountOAuthArgument("https://accounts.cloud.databricks.com", "xyz") - assert.NoError(t, err) - p, err := u2m.NewPersistentAuth( + arg, err := NewBasicAccountOAuthArgument("https://accounts.cloud.databricks.com", "xyz") + if err != nil { + t.Fatalf("NewBasicAccountOAuthArgument(): want no error, got %v", err) + } + p, err := NewPersistentAuth( ctx, - u2m.WithTokenCache(cache), - u2m.WithHttpClient(&http.Client{ + WithTokenCache(cache), + WithHttpClient(&http.Client{ Transport: fixtures.SliceTransport{ { Method: "POST", @@ -145,21 +172,30 @@ func TestToken_ReturnsError(t *testing.T) { }, }, }), - u2m.WithOAuthEndpointSupplier(MockOAuthEndpointSupplier{}), - u2m.WithOAuthArgument(arg), + WithOAuthEndpointSupplier(MockOAuthEndpointSupplier{}), + WithOAuthArgument(arg), ) - require.NoError(t, err) + if err != nil { + t.Errorf("NewPersistentAuth(): want no error, got %v", err) + } defer p.Close() tok, err := p.Token() - assert.Nil(t, tok) - assert.ErrorContains(t, err, "Invalid Client (error code: invalid_grant)") + + if tok != nil { + t.Errorf("p.Token(): want nil, got %v", tok) + } + if !strings.Contains(err.Error(), "Invalid Client (error code: invalid_grant)") { + t.Errorf("p.Token(): want error containing 'Invalid Client (error code: invalid_grant)', got %v", err) + } } func TestToken_ReturnsInvalidRefreshTokenError(t *testing.T) { ctx := context.Background() cache := &tokenCacheMock{ lookup: func(key string) (*oauth2.Token, error) { - assert.Equal(t, "https://accounts.cloud.databricks.com/oidc/accounts/xyz", key) + if key != "https://accounts.cloud.databricks.com/oidc/accounts/xyz" { + t.Fatalf("lookup(): want key 'https://accounts.cloud.databricks.com/oidc/accounts/xyz', got %s", key) + } return &oauth2.Token{ AccessToken: "expired", RefreshToken: "cde", @@ -167,12 +203,14 @@ func TestToken_ReturnsInvalidRefreshTokenError(t *testing.T) { }, nil }, } - arg, err := u2m.NewBasicAccountOAuthArgument("https://accounts.cloud.databricks.com", "xyz") - assert.NoError(t, err) - p, err := u2m.NewPersistentAuth( + arg, err := NewBasicAccountOAuthArgument("https://accounts.cloud.databricks.com", "xyz") + if err != nil { + t.Fatalf("NewBasicAccountOAuthArgument(): want no error, got %v", err) + } + p, err := NewPersistentAuth( ctx, - u2m.WithTokenCache(cache), - u2m.WithHttpClient(&http.Client{ + WithTokenCache(cache), + WithHttpClient(&http.Client{ Transport: fixtures.SliceTransport{ { Method: "POST", @@ -182,15 +220,21 @@ func TestToken_ReturnsInvalidRefreshTokenError(t *testing.T) { }, }, }), - u2m.WithOAuthEndpointSupplier(MockOAuthEndpointSupplier{}), - u2m.WithOAuthArgument(arg), + WithOAuthEndpointSupplier(MockOAuthEndpointSupplier{}), + WithOAuthArgument(arg), ) - require.NoError(t, err) + if err != nil { + t.Errorf("NewPersistentAuth(): want no error, got %v", err) + } defer p.Close() tok, err := p.Token() - assert.Nil(t, tok) - target := &u2m.InvalidRefreshTokenError{} - assert.True(t, errors.As(err, &target)) + if tok != nil { + t.Fatalf("p.Token(): want nil, got %v", tok) + } + target := &InvalidRefreshTokenError{} + if !errors.As(err, &target) { + t.Fatalf("p.Token(): want error of type InvalidRefreshTokenError, got %v", err) + } } func TestChallenge(t *testing.T) { @@ -202,7 +246,9 @@ func TestChallenge(t *testing.T) { if err != nil { return err } - assert.Equal(t, "/oidc/accounts/xyz/v1/authorize", u.Path) + if u.Path != "/oidc/accounts/xyz/v1/authorize" { + t.Fatalf("browser(): want path '/oidc/accounts/xyz/v1/authorize', got %s", u.Path) + } // for now we're ignoring asserting the fields of the redirect query := u.Query() browserOpened <- query.Get("state") @@ -210,19 +256,28 @@ func TestChallenge(t *testing.T) { } cache := &tokenCacheMock{ store: func(key string, tok *oauth2.Token) error { - assert.Equal(t, "https://accounts.cloud.databricks.com/oidc/accounts/xyz", key) - assert.Equal(t, "__THAT__", tok.AccessToken) - assert.Equal(t, "__SOMETHING__", tok.RefreshToken) + if key != "https://accounts.cloud.databricks.com/oidc/accounts/xyz" { + t.Fatalf("store(): want key 'https://accounts.cloud.databricks.com/oidc/accounts/xyz', got %s", key) + } + if tok.AccessToken != "__THAT__" { + t.Fatalf("store(): want access token '__THAT__', got %s", tok.AccessToken) + } + if tok.RefreshToken != "__SOMETHING__" { + t.Fatalf("store(): want refresh token '__SOMETHING__', got %s", tok.RefreshToken) + } return nil }, } - arg, err := u2m.NewBasicAccountOAuthArgument("https://accounts.cloud.databricks.com", "xyz") - assert.NoError(t, err) - p, err := u2m.NewPersistentAuth( + arg, err := NewBasicAccountOAuthArgument("https://accounts.cloud.databricks.com", "xyz") + if err != nil { + t.Fatalf("NewBasicAccountOAuthArgument(): want no error, got %v", err) + } + + p, err := NewPersistentAuth( ctx, - u2m.WithTokenCache(cache), - u2m.WithBrowser(browser), - u2m.WithHttpClient(&http.Client{ + WithTokenCache(cache), + WithBrowser(browser), + WithHttpClient(&http.Client{ Transport: fixtures.SliceTransport{ { Method: "POST", @@ -234,10 +289,12 @@ func TestChallenge(t *testing.T) { }, }, }), - u2m.WithOAuthEndpointSupplier(MockOAuthEndpointSupplier{}), - u2m.WithOAuthArgument(arg), + WithOAuthEndpointSupplier(MockOAuthEndpointSupplier{}), + WithOAuthArgument(arg), ) - require.NoError(t, err) + if err != nil { + t.Errorf("NewPersistentAuth(): want no error, got %v", err) + } defer p.Close() errc := make(chan error) @@ -249,12 +306,18 @@ func TestChallenge(t *testing.T) { state := <-browserOpened resp, err := http.Get(fmt.Sprintf("http://localhost:8020?code=__THIS__&state=%s", state)) - require.NoError(t, err) + if err != nil { + t.Fatalf("http.Get(): want no error, got %v", err) + } defer resp.Body.Close() - assert.Equal(t, 200, resp.StatusCode) + if resp.StatusCode != 200 { + t.Fatalf("http.Get(): want status code 200, got %d", resp.StatusCode) + } err = <-errc - assert.NoError(t, err) + if err != nil { + t.Fatalf("p.Challenge(): want no error, got %v", err) + } } func TestChallenge_ReturnsErrorOnFailure(t *testing.T) { @@ -265,16 +328,23 @@ func TestChallenge_ReturnsErrorOnFailure(t *testing.T) { if err != nil { return err } - assert.Equal(t, "/oidc/accounts/xyz/v1/authorize", u.Path) + if u.Path != "/oidc/accounts/xyz/v1/authorize" { + t.Fatalf("browser(): want path '/oidc/accounts/xyz/v1/authorize', got %s", u.Path) + } // for now we're ignoring asserting the fields of the redirect query := u.Query() browserOpened <- query.Get("state") return nil } - arg, err := u2m.NewBasicAccountOAuthArgument("https://accounts.cloud.databricks.com", "xyz") - assert.NoError(t, err) - p, err := u2m.NewPersistentAuth(ctx, u2m.WithBrowser(browser), u2m.WithOAuthArgument(arg)) - require.NoError(t, err) + arg, err := NewBasicAccountOAuthArgument("https://accounts.cloud.databricks.com", "xyz") + if err != nil { + t.Fatalf("NewBasicAccountOAuthArgument(): want no error, got %v", err) + } + + p, err := NewPersistentAuth(ctx, WithBrowser(browser), WithOAuthArgument(arg)) + if err != nil { + t.Errorf("NewPersistentAuth(): want no error, got %v", err) + } defer p.Close() errc := make(chan error) @@ -285,12 +355,21 @@ func TestChallenge_ReturnsErrorOnFailure(t *testing.T) { }() <-browserOpened - resp, err := http.Get( - "http://localhost:8020?error=access_denied&error_description=Policy%20evaluation%20failed%20for%20this%20request") - require.NoError(t, err) + resp, err := http.Get("http://localhost:8020?error=access_denied&error_description=Policy%20evaluation%20failed%20for%20this%20request") + if err != nil { + t.Fatalf("http.Get(): want no error, got %v", err) + } defer resp.Body.Close() - assert.Equal(t, 400, resp.StatusCode) + + if resp.StatusCode != 400 { + t.Fatalf("http.Get(): want status code 400, got %d", resp.StatusCode) + } err = <-errc - assert.EqualError(t, err, "authorize: access_denied: Policy evaluation failed for this request") + if err == nil { + t.Fatalf("p.Challenge(): want error, got nil") + } + if !strings.Contains(err.Error(), "authorize: access_denied: Policy evaluation failed for this request") { + t.Fatalf("p.Challenge(): want error containing 'authorize: access_denied: Policy evaluation failed for this request', got %v", err) + } } From ddb1c648424d4e26714e4a9701582d7aed7e473a Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Thu, 26 Jun 2025 14:23:45 +0000 Subject: [PATCH 2/3] Assign free port --- credentials/u2m/persistent_auth.go | 35 +++++++++++++++----- credentials/u2m/persistent_auth_test.go | 44 +++++++++++++++++++++++-- 2 files changed, 69 insertions(+), 10 deletions(-) diff --git a/credentials/u2m/persistent_auth.go b/credentials/u2m/persistent_auth.go index 7e28a25f5..ce2a904d6 100644 --- a/credentials/u2m/persistent_auth.go +++ b/credentials/u2m/persistent_auth.go @@ -26,7 +26,8 @@ const ( appClientID = "databricks-cli" // appRedirectAddr is the default address for the OAuth2 callback server. - appRedirectAddr = "localhost:8020" + // Using ":0" tells the system to pick a random available port. + appRedirectAddr = "localhost:0" // listenerTimeout is the maximum amount of time to acquire listener on // appRedirectAddr. @@ -57,6 +58,11 @@ type PersistentAuth struct { // ctx is the context to use for underlying operations. This is needed in // order to implement the oauth2.TokenSource interface. ctx context.Context + // redirectAddr is the redirect address for OAuth2 callbacks. The value is + // set to localhost:PORT by startListener which will dynamically assign a + // random port. If a value is already provided, it will be used instead + // (e.g. for testing). + redirectAddr string } type PersistentAuthOption func(*PersistentAuth) @@ -128,9 +134,6 @@ func NewPersistentAuth(ctx context.Context, opts ...PersistentAuthOption) (*Pers return nil, fmt.Errorf("cache: %w", err) } } - if p.oAuthArgument == nil { - return nil, errors.New("missing OAuthArgument") - } if err := p.validateArg(); err != nil { return nil, err } @@ -162,7 +165,9 @@ func (a *PersistentAuth) Token() (t *oauth2.Token, err error) { return nil, fmt.Errorf("token refresh: %w", err) } } - // do not print refresh token to end-user + + // Do not include the refresh token for security reasons. Refresh tokens are + // long-lived credentials that we do not want to expose unnecessarily. t.RefreshToken = "" return t, nil } @@ -269,12 +274,19 @@ func (a *PersistentAuth) Challenge() error { // startListener starts a listener on appRedirectAddr, retrying if the address // is already in use. func (a *PersistentAuth) startListener(ctx context.Context) error { + // Use the value of redirectURL if it is already set. This is only expected + // in tests to set a fixed redirect URL. + addr := a.redirectAddr + if addr == "" { + addr = appRedirectAddr + } + listener, err := retries.Poll(ctx, listenerTimeout, func() (*net.Listener, *retries.Err) { var lc net.ListenConfig - l, err := lc.Listen(ctx, "tcp", appRedirectAddr) + l, err := lc.Listen(ctx, "tcp", addr) if err != nil { - logger.Debugf(ctx, "failed to listen on %s: %v, retrying", appRedirectAddr, err) + logger.Debugf(ctx, "failed to listen on %s: %v, retrying", addr, err) return nil, retries.Continue(err) } return &l, nil @@ -283,6 +295,10 @@ func (a *PersistentAuth) startListener(ctx context.Context) error { return fmt.Errorf("listener: %w", err) } a.ln = *listener + + // Get the actual address that was assigned (including the port). + a.redirectAddr = a.ln.Addr().String() + logger.Debugf(ctx, "OAuth callback server listening on %s", a.redirectAddr) return nil } @@ -296,6 +312,9 @@ func (a *PersistentAuth) Close() error { // validateArg ensures that the OAuthArgument is either a WorkspaceOAuthArgument // or an AccountOAuthArgument. func (a *PersistentAuth) validateArg() error { + if a.oAuthArgument == nil { + return errors.New("missing OAuthArgument") + } _, isWorkspaceArg := a.oAuthArgument.(WorkspaceOAuthArgument) _, isAccountArg := a.oAuthArgument.(AccountOAuthArgument) if !isWorkspaceArg && !isAccountArg { @@ -331,7 +350,7 @@ func (a *PersistentAuth) oauth2Config() (*oauth2.Config, error) { TokenURL: endpoints.TokenEndpoint, AuthStyle: oauth2.AuthStyleInParams, }, - RedirectURL: fmt.Sprintf("http://%s", appRedirectAddr), + RedirectURL: fmt.Sprintf("http://%s", a.redirectAddr), Scopes: scopes, }, nil } diff --git a/credentials/u2m/persistent_auth_test.go b/credentials/u2m/persistent_auth_test.go index 402ecdecc..27048ead7 100644 --- a/credentials/u2m/persistent_auth_test.go +++ b/credentials/u2m/persistent_auth_test.go @@ -6,6 +6,7 @@ import ( "fmt" "net/http" "net/url" + "regexp" "strings" "testing" "time" @@ -297,6 +298,9 @@ func TestChallenge(t *testing.T) { } defer p.Close() + // Set a fixed redirect URL for the test. + p.redirectAddr = "localhost:1337" + errc := make(chan error) go func() { err := p.Challenge() @@ -305,7 +309,7 @@ func TestChallenge(t *testing.T) { }() state := <-browserOpened - resp, err := http.Get(fmt.Sprintf("http://localhost:8020?code=__THIS__&state=%s", state)) + resp, err := http.Get(fmt.Sprintf("http://localhost:1337?code=__THIS__&state=%s", state)) if err != nil { t.Fatalf("http.Get(): want no error, got %v", err) } @@ -347,6 +351,8 @@ func TestChallenge_ReturnsErrorOnFailure(t *testing.T) { } defer p.Close() + p.redirectAddr = "localhost:1337" // set a fixed redirect URL for the test + errc := make(chan error) go func() { err := p.Challenge() @@ -355,7 +361,7 @@ func TestChallenge_ReturnsErrorOnFailure(t *testing.T) { }() <-browserOpened - resp, err := http.Get("http://localhost:8020?error=access_denied&error_description=Policy%20evaluation%20failed%20for%20this%20request") + resp, err := http.Get("http://localhost:1337?error=access_denied&error_description=Policy%20evaluation%20failed%20for%20this%20request") if err != nil { t.Fatalf("http.Get(): want no error, got %v", err) } @@ -373,3 +379,37 @@ func TestChallenge_ReturnsErrorOnFailure(t *testing.T) { t.Fatalf("p.Challenge(): want error containing 'authorize: access_denied: Policy evaluation failed for this request', got %v", err) } } + +// Verifies that startListener assigns a random port to the redirectAddr. +func TestPersistentAuth_startListener_useDifferentPorts(t *testing.T) { + ctx := context.Background() + arg, err := NewBasicAccountOAuthArgument("https://accounts.cloud.databricks.com", "xyz") + if err != nil { + t.Fatalf("NewBasicAccountOAuthArgument(): want no error, got %v", err) + } + + p1, err := NewPersistentAuth(ctx, WithOAuthArgument(arg)) + if err != nil { + t.Fatalf("NewPersistentAuth(): want no error, got %v", err) + } + defer p1.Close() + + p2, err := NewPersistentAuth(ctx, WithOAuthArgument(arg)) + if err != nil { + t.Fatalf("NewPersistentAuth(): want no error, got %v", err) + } + defer p2.Close() + + p1.startListener(ctx) + p2.startListener(ctx) + + if !regexp.MustCompile(`^127\.0\.0\.1:\d+$`).MatchString(p1.redirectAddr) { + t.Errorf("p1.redirectAddr should be random localhost port, got %s", p1.redirectAddr) + } + if !regexp.MustCompile(`^127\.0\.0\.1:\d+$`).MatchString(p2.redirectAddr) { + t.Errorf("p2.redirectAddr should be random localhost port, got %s", p2.redirectAddr) + } + if p1.redirectAddr == p2.redirectAddr { + t.Errorf("p1.redirectURL and p2.redirectURL should be different, got %s", p1.redirectAddr) + } +} From 0a2fa8fa00893cb36b5fa56f512feb6671630176 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Thu, 26 Jun 2025 15:20:58 +0000 Subject: [PATCH 3/3] Changelog --- NEXT_CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index cd81cd7c0..c76e732c9 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -4,6 +4,8 @@ ### New Features and Improvements +* Use a free port in `u2m` authentication flows rather than 8020. + ### Bug Fixes ### Documentation