login: drop ireturn nolint via concrete flow; tidy output spacing · Entire
login: drop ireturn nolint via concrete flow; tidy output spacing
ec2a59f→main·
toothbrush·1mo ago·3 files·+44 added/-83 removed
Avoid the ireturn lint suppression by following "accept interfaces, return structs" (as the device-flow shim already does):
- auth.BrowserAuthFlow is now a concrete struct (was an interface); StartBrowserAuth returns *BrowserAuthFlow.
- runBrowserLogin accepts a cli-local browserAuthFlow interface plus the base URL, with StartBrowserAuth moved up into newLoginCmd. The fake flow satisfies the local interface; no nolint anywhere.
Output: one space after "Logging in to:" and a single blank line before "Waiting for sign-in..." (the leading newline doubled up with the post-Enter newline).
Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com
Sessions
e72efb851121View transcript
Changes
3
cmd/entire/cli
auth
Mclient.go+17/-28
Mlogin.go+22/-18
Mlogin_test.go+5/-37
92 unmodified lines
// BrowserAuthFlow is one in-progress loopback authorization-code login. // It wraps an authcode.Flow so login.go can depend on a small interface // (and fake it in tests) rather than the concrete library type that owns // a live loopback listener. type BrowserAuthFlow interface { // AuthorizationURL is the URL to open in the user's browser. AuthorizationURL() string // Wait blocks until the browser is redirected to the loopback // listener, returning the authorization code. Wait(ctx context.Context) (code string, err error) // Exchange redeems code for access + refresh tokens. Exchange(ctx context.Context, code string) (accessToken, refreshToken string, err error) // Close tears down the loopback listener. Safe to call after Wait. Close() error }
// browserAuthFlow adapts *authcode.Flow to BrowserAuthFlow, flattening the // TokenSet to the (access, refresh) pair login.go persists — mirroring how // PollDeviceAuth flattens the device-flow result. type browserAuthFlow struct { // BrowserAuthFlow is one in-progress loopback authorization-code login. It // wraps an authcode.Flow, flattening the TokenSet to the (access, refresh) // pair login.go persists — mirroring how PollDeviceAuth flattens the // device-flow result. login.go depends on a small local interface that this // concrete type satisfies, so it can fake the flow in tests. type BrowserAuthFlow struct { inner *authcode.Flow }
func (f *browserAuthFlow) AuthorizationURL() string { return f.inner.AuthorizationURL }
func (f *browserAuthFlow) Wait(ctx context.Context) (string, error) { return f.inner.Wait(ctx) //nolint:wrapcheck // shim preserves the lib's wrapped errors verbatim for errors.Is }
func (f *browserAuthFlow) Exchange(ctx context.Context, code string) (string, string, error) { ts, err := f.inner.Exchange(ctx, code) if err != nil { return "", "", err //nolint:wrapcheck // shim returns authcode errors verbatim so callers can errors.Is on sentinels } return ts.AccessToken, ts.RefreshToken, nil }
func (f *browserAuthFlow) Close() error { return f.inner.Close() //nolint:wrapcheck // shutdown error is best-effort; caller logs at most }
// StartBrowserAuth begins the loopback authorization-code flow: it binds a // local listener and returns a flow carrying the browser URL to open. func (c *Client) StartBrowserAuth(ctx context.Context) (BrowserAuthFlow, error) { f, err := c.browser.Start(ctx) if err != nil { return nil, err //nolint:wrapcheck // shim returns authcode errors verbatim so callers can errors.Is on sentinels } return &browserAuthFlow{inner: f}, nil }
// BaseURL returns the issuer base URL this client talks to.
Mcmd/entire/cli/auth/client.go+17/-28
46 unmodified lines
type browserAuthClient interface { StartBrowserAuth(ctx context.Context) (auth.BrowserAuthFlow, error) BaseURL() string }
func newLoginCmd() *cobra.Command { // the same both-flows-with-fallback shape gh / gcloud / aws sso // ship. --device forces the device flow explicitly. }
// runBrowserLogin runs the loopback authorization-code flow: open the // authorization URL in the user's browser, wait for the redirect back to // the local listener, then exchange the code for tokens. func runBrowserLogin(ctx context.Context, outW, errW io.Writer, client browserAuthClient, openURL browserOpenFunc) error { flow, err := client.StartBrowserAuth(ctx) if err != nil { return fmt.Errorf("start login: %w", err) } defer func() { _ = flow.Close() }() fmt.Fprintf(outW, "Logging in to: %s\n\n", client.BaseURL()) fmt.Fprint(outW, "Press Enter to open in browser...")
code, err := flow.Wait(ctx) if err != nil { return fmt.Errorf("complete login: %w", err) } return persistLogin(outW, errW, client.BaseURL(), token, refreshToken) }
// persistLogin validates the freshly-issued access token, saves it to the
Mcmd/entire/cli/login.go+22/-18
301 unmodified lines
// fakeBrowserFlow implements auth.BrowserAuthFlow for unit tests. type fakeBrowserFlow struct { authURL string waitCode string }
// fakeBrowserClient implements browserAuthClient for unit tests. type fakeBrowserClient struct { flow *fakeBrowserFlow startErr error }
func (c *fakeBrowserClient) StartBrowserAuth(context.Context) (auth.BrowserAuthFlow, error) { if c.startErr != nil { return nil, c.startErr } return c.flow, nil }
func (c *fakeBrowserClient) BaseURL() string { return "http://test" }
func TestShouldUseBrowserLogin(t *testing.T) { t.Parallel() }