fix: address verified review findings (auth-config-doctor-1)

Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
bircni committed 2026-10-18 17:38:52 +02:00
1 parent e15ea907be
commit 0d47b3f149
7 files changed
+113 -6

No files matched your search

+1 -1
View File
@@ -111,7 +111,7 @@ Managing accounts:
| `GITEA_SERVER_URL`, `GITEA_SERVER_TOKEN`, `GITEA_SERVER_USER`, `GITEA_SERVER_PASSWORD`, `GITEA_SERVER_OTP`, `GITEA_SCOPES` | removed with `tea login add` |
New variables: `TEA_CONFIG_DIR`, `TEA_HOST`, `TEA_USER`, `TEA_REPO`, `TEA_EDITOR`,
`TEA_PAGER` and `TEA_COLOR`; see `tea config --help`. Environment credentials are
`TEA_PAGER` and `TEA_COLOR`; see `tea help environment`. Environment credentials are
never written to disk.
### Git credential helper
+12
View File
@@ -335,3 +335,15 @@ func TestCompletionTargetAcceptsABareTokenHost(t *testing.T) {
require.NoError(t, err)
require.Equal(t, instance, resolved.Host)
}
func TestAccountCompletionUsesInstanceOfRepository(t *testing.T) {
deps, out, _ := testDeps(t)
deps.Git = &fakeGit{results: map[string]string{"remote": "origin", "remote get-url origin": "https://a.example.org/alice/project.git"}}
configureHost(t, deps, "https://a.example.org", settings.Host{User: "alice", Users: tokenAccounts(settings.StoreKeyring, "alice", "adam")})
configureHost(t, deps, "https://b.example.org", settings.Host{User: "bob", Users: tokenAccounts(settings.StoreKeyring, "bob")})
require.NoError(t, deps.Settings.Update(settings.TargetPreferences, func(s *settings.Snapshot) error { s.Preferences.DefaultHost = "https://b.example.org"; return nil }))
for _, command := range []string{"switch", "logout"} {
values, _ := complete(t, deps, out, "auth", command, "")
require.Equal(t, []string{"adam\thttps://a.example.org", "alice\thttps://a.example.org (active)"}, values, command)
}
}
+28 -4
View File
@@ -66,12 +66,15 @@ func (a *application) sshPassphrase(ctx context.Context, policy terminal.Policy,
}
// instanceTarget selects the instance and account an auth command acts on, without authenticating:
// with no --host, the instance of the working directory's repository when tea has accounts there,
// else the one other commands select.
// with no --host, the instance of the working directory's repository when tea has accounts there
// (and the account named by --user or TEA_USER, if any), else the one other commands select.
func (a *application) instanceTarget(ctx context.Context) (target, error) {
if a.options.host == "" {
if t, err := a.resolveTarget(ctx, true, false); err == nil && len(t.host.Users) > 0 {
return t, nil
user, _ := a.selectedUser()
if _, ok := matchAccount(t.host.Users, user); user == "" || ok {
return t, nil
}
}
}
return a.resolveTarget(ctx, false, false)
@@ -431,6 +434,9 @@ $ tea auth status --host gitea.com --json`,
if err != nil {
return err
}
if s.Hosts.Hosts, err = a.environmentHosts(s.Hosts.Hosts); err != nil {
return err
}
selectedHost := ""
if a.options.host != "" {
if selectedHost, err = settings.ResolveHost(a.options.host, s.Hosts.Hosts); err != nil {
@@ -496,7 +502,16 @@ func (a *application) accountStatus(ctx context.Context, s settings.Snapshot, in
if verify {
err := errors.New("credential unavailable")
if row.Available {
row.Scopes, err = a.verifyAccount(ctx, a.policy(s), instance, user, host, account, credential.AccessToken)
err = nil
if account.AuthMethod == settings.AuthOAuth {
// An expired access token is not a broken credential: commands refresh it before use.
if credential, err = a.refreshedCredential(ctx, instance, user, host); err == nil && !credential.ExpiresAt.IsZero() {
row.ExpiresAt = &credential.ExpiresAt
}
}
if err == nil {
row.Scopes, err = a.verifyAccount(ctx, a.policy(s), instance, user, host, account, credential.AccessToken)
}
}
verified := err == nil
row.Verified = &verified
@@ -507,6 +522,15 @@ func (a *application) accountStatus(ctx context.Context, s settings.Snapshot, in
return row
}
// refreshedCredential loads the OAuth credential of an account, refreshing it first when it expires.
func (a *application) refreshedCredential(ctx context.Context, instance, user string, host settings.Host) (auth.Credential, error) {
manager, err := auth.NewManager(a.deps.Settings, a.deps.Credentials)
if err != nil {
return auth.Credential{}, err
}
return manager.Refresh(ctx, instance, user, a.deps.Client(instance, "", host.Insecure).HTTP, false, a.deps.Now())
}
// environmentStatus reports the GITEA_TOKEN of the environment, when one is set.
func (a *application) environmentStatus(ctx context.Context, s settings.Snapshot, verify bool) (authStatusRow, bool) {
token := a.deps.Getenv("GITEA_TOKEN")
+57
View File
@@ -496,6 +496,30 @@ func TestAuthStatusVerifyChecksStoredCredentials(t *testing.T) {
require.Equal(t, server.URL+"\talice\ttrue\toauth\tfile\ttrue\ttrue\t2030-01-02T03:04:05Z\t\n", out.String())
}
func TestAuthStatusVerifyRefreshesExpiredOAuthToken(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/login/oauth/access_token":
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{"access_token": "fresh", "refresh_token": "refresh2", "expires_in": 3600, "token_type": "Bearer"})
case "/api/v1/token":
_, _ = w.Write([]byte(`{"scopes":["all"]}`))
default:
if r.Header.Get("Authorization") != "token fresh" {
w.WriteHeader(http.StatusUnauthorized)
return
}
_, _ = w.Write([]byte(`{"login":"alice"}`))
}
}))
defer server.Close()
deps, out, _ := testDeps(t)
require.NoError(t, deps.Credentials.Save(server.URL, "alice", settings.StoreFile, auth.Credential{AccessToken: "stale", RefreshToken: "refresh", ExpiresAt: time.Now().Add(-time.Hour)}))
configureHost(t, deps, server.URL, settings.Host{User: "alice", Users: map[string]settings.Account{"alice": {AuthMethod: settings.AuthOAuth, CredentialStore: settings.StoreFile}}})
require.NoError(t, Execute(context.Background(), deps, []string{"auth", "status", "--verify", "--json=user,verified,error"}))
require.JSONEq(t, `[{"user":"alice","verified":true,"error":""}]`, out.String())
}
func TestOAuthLoginOpensAuthorizationURLInBrowser(t *testing.T) {
opened := make(chan string, 1)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -784,3 +808,36 @@ func TestLogoutAsksGitToForgetTheAccount(t *testing.T) {
err := Execute(context.Background(), deps, []string{"auth", "logout", "carol", "--yes"})
require.EqualError(t, err, `account "carol" is not logged in on `+instance+" (accounts: bob)")
}
func TestLogoutFindsNamedAccountOnAnotherInstanceThanTheClone(t *testing.T) {
deps, _, _ := testDeps(t)
deps.Credentials.Keyring = &statusKeyring{}
deps.Git = &fakeGit{results: map[string]string{"remote": "origin", "remote get-url origin": "https://a.example.org/alice/project.git"}}
configureHost(t, deps, "https://a.example.org", settings.Host{User: "alice", Users: tokenAccounts(settings.StoreKeyring, "alice")})
configureHost(t, deps, "https://b.example.org", settings.Host{User: "bob", Users: tokenAccounts(settings.StoreKeyring, "bob")})
require.NoError(t, deps.Settings.Update(settings.TargetPreferences, func(s *settings.Snapshot) error { s.Preferences.DefaultHost = "https://b.example.org"; return nil }))
require.NoError(t, Execute(context.Background(), deps, []string{"auth", "logout", "bob", "--yes", "--no-input"}))
snapshot, err := deps.Settings.Load()
require.NoError(t, err)
require.NotContains(t, snapshot.Hosts.Hosts["https://b.example.org"].Users, "bob")
require.Contains(t, snapshot.Hosts.Hosts["https://a.example.org"].Users, "alice")
}
func TestAuthStatusVerifyHonorsInstanceInsecureForEnvironmentToken(t *testing.T) {
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api/v1/token" {
_, _ = w.Write([]byte(`{"scopes":["all"]}`))
return
}
_, _ = w.Write([]byte(`{"login":"alice"}`))
}))
defer server.Close()
environment := map[string]string{"GITEA_INSTANCE_URL": server.URL, "GITEA_TOKEN": "secret"}
deps, out, _ := testDeps(t)
deps.Getenv = envFunc(environment)
require.Error(t, Execute(context.Background(), deps, []string{"auth", "status", "--verify"}))
out.Reset()
environment["GITEA_INSTANCE_INSECURE"] = "true"
require.NoError(t, Execute(context.Background(), deps, []string{"auth", "status", "--verify", "--json=user,verified"}))
require.JSONEq(t, `[{"user":"alice","verified":true}]`, out.String())
}
+1 -1
View File
@@ -194,7 +194,7 @@ func (a *application) completeAccounts(cmd *cobra.Command, _ []string, toComplet
return nil, cobra.ShellCompDirectiveNoFileComp
}
hosts := snapshot.Hosts.Hosts
if t, err := a.resolveTarget(ctx, false, false); err == nil {
if t, err := a.instanceTarget(ctx); err == nil {
hosts = map[string]settings.Host{t.Host: hosts[t.Host]}
}
accounts := []cobra.Completion{}
+2
View File
@@ -54,6 +54,8 @@ const doctorTimeout = 10 * time.Second
func (a *application) doctorCommand() *cobra.Command {
return &cobra.Command{
Use: "doctor", Short: "Check tea's setup for this directory", Args: argsExact(0),
// doctor loads the settings itself, to report a broken file instead of failing on it.
Annotations: map[string]string{"tea.offline": "true"},
Long: `Check the setup that commands in this directory use: the settings files, the instance
and whether it answers, the account and its credential, the repository, and Git. Each
problem comes with the command that fixes it. tea doctor fails when a check fails.`,
+12
View File
@@ -7,6 +7,8 @@ import (
"context"
"encoding/json"
"net/http"
"os"
"path/filepath"
"strings"
"testing"
@@ -115,6 +117,16 @@ func TestDoctorReportsEveryCheckInsteadOfFailingFirst(t *testing.T) {
}
}
func TestDoctorReportsBrokenSettingsFile(t *testing.T) {
deps, out, _ := testDeps(t)
deps.Git = &fakeGit{results: map[string]string{"--version": "git version 2.50.1"}}
require.NoError(t, os.WriteFile(filepath.Join(deps.Settings.Dir, "hosts.yml"), []byte("hosts: [unclosed"), 0o600))
err := Execute(context.Background(), deps, []string{"doctor", "--json"})
require.Error(t, err)
require.Contains(t, out.String(), `"name":"Config","status":"failed"`)
require.Contains(t, out.String(), `"name":"Git"`)
}
func TestDoctorShowsChecksOnATerminal(t *testing.T) {
server := newAuthenticatedServer(doctorServer(t))
defer server.Close()