From 574ccda410ac833b27f02944cd4cf08cf19ddc90 Mon Sep 17 00:00:00 2001 From: Frank Lin <2888279+flin-8@users.noreply.github.com> Date: Tue, 8 Sep 2026 15:39:36 +1000 Subject: [PATCH] fix: stop login --ignore-ssl-errors panicking on a wrapped transport The client factory hands login a client whose transport is the spinner round tripper, so type-asserting it to *http.Transport panics. Apply the setting underneath any wrappers instead, leaving them in place. Fixes #725 Co-Authored-By: Claude Opus 5 --- pkg/cmd/login/login.go | 20 ++++++++++++++- pkg/cmd/login/login_internal_test.go | 38 ++++++++++++++++++++++++++++ pkg/cmd/login/login_test.go | 16 ++++++++++++ 3 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 pkg/cmd/login/login_internal_test.go diff --git a/pkg/cmd/login/login.go b/pkg/cmd/login/login.go index d6ed863d..02019cfe 100644 --- a/pkg/cmd/login/login.go +++ b/pkg/cmd/login/login.go @@ -131,7 +131,9 @@ func loginRun(cmd *cobra.Command, f factory.Factory, isPromptEnabled bool, ask q httpClient.Transport = &http.Transport{} } - httpClient.Transport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + if err := skipTlsVerification(httpClient.Transport); err != nil { + return err + } } if inputs.apiKey != "" { @@ -421,3 +423,19 @@ func testLogin(cmd *cobra.Command, httpClient *http.Client, server string, crede return nil } + +// The client factory wraps the transport in a spinner, so the setting has to be +// applied to the transport underneath rather than to the wrapper. +func skipTlsVerification(roundTripper http.RoundTripper) error { + switch transport := roundTripper.(type) { + case *http.Transport: + transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + return nil + case *apiclient.SpinnerRoundTripper: + return skipTlsVerification(transport.Next) + default: + // Better to say so than to quietly leave verification on after the + // caller asked for the opposite. + return fmt.Errorf("cannot ignore SSL errors: unsupported HTTP transport %T", roundTripper) + } +} diff --git a/pkg/cmd/login/login_internal_test.go b/pkg/cmd/login/login_internal_test.go new file mode 100644 index 00000000..ca5e6ace --- /dev/null +++ b/pkg/cmd/login/login_internal_test.go @@ -0,0 +1,38 @@ +package login + +import ( + "net/http" + "testing" + + "github.com/OctopusDeploy/cli/pkg/apiclient" + "github.com/stretchr/testify/assert" +) + +func TestSkipTlsVerification_OnATransport(t *testing.T) { + transport := &http.Transport{} + + assert.NoError(t, skipTlsVerification(transport)) + assert.True(t, transport.TLSClientConfig.InsecureSkipVerify) +} + +// The transport the client factory hands out is always spinner-wrapped, which is +// what the old type assertion tripped over. +func TestSkipTlsVerification_ThroughTheSpinner(t *testing.T) { + spinner := apiclient.NewSpinnerRoundTripper(nil) + spinner.Next = &http.Transport{} + + assert.NoError(t, skipTlsVerification(spinner)) + assert.True(t, spinner.Next.(*http.Transport).TLSClientConfig.InsecureSkipVerify) +} + +func TestSkipTlsVerification_ReportsAnUnknownTransport(t *testing.T) { + err := skipTlsVerification(unknownTransport{}) + + assert.ErrorContains(t, err, "unsupported HTTP transport login.unknownTransport") +} + +type unknownTransport struct{} + +func (unknownTransport) RoundTrip(*http.Request) (*http.Response, error) { + return nil, nil +} diff --git a/pkg/cmd/login/login_test.go b/pkg/cmd/login/login_test.go index 97910e22..230c63dc 100644 --- a/pkg/cmd/login/login_test.go +++ b/pkg/cmd/login/login_test.go @@ -146,6 +146,22 @@ func TestLogin_ApiKey(t *testing.T) { assert.Empty(t, fac.ConfigProvider.Get(constants.ConfigAccessToken)) }}, + {"non-interactive: ignoring ssl errors no longer panics on a wrapped transport", func(t *testing.T, fac *testutil.MockFactory, api *testutil.MockHttpServer, qa *testutil.AskMocker, rootCmd *cobra.Command, stdOut *bytes.Buffer, stdErr *bytes.Buffer) { + currentHost := fac.GetCurrentHost() + + cmdReceiver := testutil.GoBegin2(func() (*cobra.Command, error) { + defer api.Close() + rootCmd.SetArgs([]string{"login", "--server", currentHost, "--api-key", "API-APIKEY01", "--ignore-ssl-errors", "--no-prompt"}) + return rootCmd.ExecuteC() + }) + + _, err := testutil.ReceivePair(cmdReceiver) + + // The mock factory's transport is not one we can reach into, so the + // flag is refused; before the fix this panicked instead. + assert.ErrorContains(t, err, "unsupported HTTP transport") + }}, + {"non-interactive: if server parameter not supplied returns error", func(t *testing.T, fac *testutil.MockFactory, api *testutil.MockHttpServer, qa *testutil.AskMocker, rootCmd *cobra.Command, stdOut *bytes.Buffer, stdErr *bytes.Buffer) { apiKey := "API-APIKEY01"