From ee227727b7c17b7b1a3c084fa84752541a05f9e7 Mon Sep 17 00:00:00 2001 From: Michael Rykov Date: Fri, 19 Jul 2024 13:50:33 +0800 Subject: [PATCH] Interactive fallback on 501 for browser login --- api/errors.go | 5 +++++ cli/api.go | 12 ++++++++--- cli/login_test.go | 37 ++++++++++++++++++++++++++++++++ internal/testutil/api.go | 46 +++++++++++++++++++++++----------------- 4 files changed, 77 insertions(+), 23 deletions(-) diff --git a/api/errors.go b/api/errors.go index 1608583..e6b4dd3 100644 --- a/api/errors.go +++ b/api/errors.go @@ -25,6 +25,9 @@ var ( // Account has an exclusive lock on the resource. Mostly used for Git repos. ErrConflict = errors.New("Locked for update by another operation. Try again later.") + + // Not implemented error for endpoints that are no longer supported + ErrNotImplemented = errors.New("This operation is not supported") ) // errorResponse is the JSON response for error from Gemfury API @@ -67,6 +70,8 @@ func StatusCodeToError(s int) error { return ErrTimeout case s == 409: return ErrConflict + case s == 501: + return ErrNotImplemented case s >= 200 && s < 300: return nil case s >= 500: diff --git a/cli/api.go b/cli/api.go index 861f556..2ae6e62 100644 --- a/cli/api.go +++ b/cli/api.go @@ -66,13 +66,19 @@ func ensureAuthenticated(cmd *cobra.Command, interactive bool) (*api.AccountResp return nil, err } - // Browser or interactive login + // Trigger browser login var resp *api.LoginResponse + if !interactive { + resp, err = browserLogin(cmd) + interactive = errors.Is(err, api.ErrNotImplemented) + } + + // Trigger interactive login if requested by user + // or if browser returned "not-implemented" if interactive { resp, err = interactiveLogin(cmd) - } else { - resp, err = browserLogin(cmd) } + if err != nil { return nil, err } diff --git a/cli/login_test.go b/cli/login_test.go index b8c271f..d85fa1b 100644 --- a/cli/login_test.go +++ b/cli/login_test.go @@ -6,6 +6,7 @@ import ( "github.com/gemfury/cli/internal/testutil" "github.com/gemfury/cli/pkg/terminal" + "net/http" "strings" "testing" ) @@ -68,6 +69,42 @@ func TestLoginCommandInteractive(t *testing.T) { } } +func TestLoginCommandInteractiveFallback(t *testing.T) { + auth := terminal.TestAuther("", "", nil) + term := terminal.NewForTest() + + // Fire up test server that returns 501 for /cli/auth + server := testutil.APIServerCustom(t, func(h *http.ServeMux) { + h.HandleFunc("/cli/auth", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) + }) + h.HandleFunc("/users/me", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(whoamiResponse)) + }) + }) + defer server.Close() + + cc := cli.TestContext(term, auth) + flags := ctx.GlobalFlags(cc) + flags.Endpoint = server.URL + + term.SetPromptResponses(map[string]string{ + "Email: ": "u@example.com", + "Password: ": "secreto", + }) + + // User requests browser, but we fall back to interactive + err := runCommandNoErr(cc, []string{"login"}) + if err != nil { + t.Error(err) + } + + outStr := string(term.OutBytes()) + if exp := "You are logged in as \"u@example.com\"\n"; !strings.Contains(outStr, exp) { + t.Errorf("Expected output to include %q, got %q", exp, outStr) + } +} + func TestLoginCommandUnauthorized(t *testing.T) { server := testutil.APIServer(t, "GET", "/users/me", whoamiResponse, 200) testCommandLoginPreCheck(t, []string{"login"}, server, noLoginOpt) diff --git a/internal/testutil/api.go b/internal/testutil/api.go index 4887ed1..7e7f575 100644 --- a/internal/testutil/api.go +++ b/internal/testutil/api.go @@ -96,25 +96,27 @@ func APIServerCustom(t *testing.T, custom func(*http.ServeMux)) *httptest.Server custom(h) // Default handler for browser auth - h.HandleFunc("/cli/auth", func(w http.ResponseWriter, r *http.Request) { - if m := r.Method; m == "POST" { - w.Write([]byte(`{ - "browser_url": "https://gemfury.com", - "cli_url": "/cli/auth?wait=true", - "token": "xyz-123" - }`)) - } else if m == "GET" { - if a := r.Header.Get("Authorization"); a != "Bearer xyz-123" { - t.Errorf("Incorrect Authorization: %q", m) + if !hasHandlerFor(h, "POST", "/cli/auth") { + h.HandleFunc("/cli/auth", func(w http.ResponseWriter, r *http.Request) { + if m := r.Method; m == "POST" { + w.Write([]byte(`{ + "browser_url": "https://gemfury.com", + "cli_url": "/cli/auth?wait=true", + "token": "xyz-123" + }`)) + } else if m == "GET" { + if a := r.Header.Get("Authorization"); a != "Bearer xyz-123" { + t.Errorf("Incorrect Authorization: %q", m) + } + w.Write([]byte(`{ + "user": { "email" : "u@example.com" }, + "token": "token-abc-123" + }`)) + } else { + t.Errorf("Incorrect method: %q", m) } - w.Write([]byte(`{ - "user": { "email" : "u@example.com" }, - "token": "token-abc-123" - }`)) - } else { - t.Errorf("Incorrect method: %q", m) - } - }) + }) + } // Default handler for interactive auth h.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) { @@ -125,8 +127,7 @@ func APIServerCustom(t *testing.T, custom func(*http.ServeMux)) *httptest.Server }) // Check if mux has a handler for "/" - rootRequest := httptest.NewRequest("GET", "/", nil) - if _, pattern := h.Handler(rootRequest); pattern == "" { + if !hasHandlerFor(h, "GET", "/") { h.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { t.Errorf("Unexpected: %s %s", r.Method, r.URL.String()) http.NotFound(w, r) @@ -135,3 +136,8 @@ func APIServerCustom(t *testing.T, custom func(*http.ServeMux)) *httptest.Server return httptest.NewServer(h) } + +func hasHandlerFor(h *http.ServeMux, method, path string) bool { + _, pattern := h.Handler(httptest.NewRequest(method, path, nil)) + return pattern != "" +}