Skip to content

Commit

Permalink
Interactive fallback on 501 for browser login
Browse files Browse the repository at this point in the history
  • Loading branch information
rykov committed Jul 19, 2024
1 parent d02deb5 commit ee22772
Show file tree
Hide file tree
Showing 4 changed files with 77 additions and 23 deletions.
5 changes: 5 additions & 0 deletions api/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
12 changes: 9 additions & 3 deletions cli/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
37 changes: 37 additions & 0 deletions cli/login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/gemfury/cli/internal/testutil"
"github.com/gemfury/cli/pkg/terminal"

"net/http"
"strings"
"testing"
)
Expand Down Expand Up @@ -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)
Expand Down
46 changes: 26 additions & 20 deletions internal/testutil/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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)
Expand All @@ -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 != ""
}

0 comments on commit ee22772

Please sign in to comment.