Files
gitea-tea/internal/app/doctor_test.go
T
bircni 014dba8512 fix(doctor): run every check instead of failing at the first problem
tea doctor printed the resolved instance, account and repository and
checked nothing; it failed with exit code 4 exactly when it was needed,
such as in a clone of an instance without an account, and with a
GITEA_TOKEN of another instance it blamed GITEA_INSTANCE_URL.

It now checks the settings files, whether the instance answers (version
and latency), whether the account's credential works and its token
scopes, whether the repository is readable (default branch), Git and
whether it asks tea for credentials, and where SSH remotes connect. Each
check shows a mark on a terminal and the command that fixes a problem;
piped, each is a tab-separated line. A GITEA_TOKEN for another instance
names both instances. doctor exits with 1 when a check fails, and --json
keeps the target fields and adds checks[].

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 23:50:44 +02:00

140 lines
5.8 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package app
import (
"context"
"encoding/json"
"net/http"
"strings"
"testing"
"gitea.dev/tea/internal/auth"
"gitea.dev/tea/internal/clierr"
"gitea.dev/tea/internal/settings"
"github.com/stretchr/testify/require"
)
// doctorServer is an instance that answers the requests of tea doctor for alice/project.
func doctorServer(t *testing.T) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/v1/version":
_, _ = w.Write([]byte(`{"version":"28.1.0"}`))
case "/api/v1/token":
_, _ = w.Write([]byte(`{"scopes":["all"]}`))
case "/api/v1/repos/alice/project":
_, _ = w.Write([]byte(`{"full_name":"alice/project","default_branch":"main"}`))
default:
t.Errorf("unexpected request %s", r.URL.Path)
w.WriteHeader(http.StatusNotFound)
}
})
}
// doctorGit is a Git checkout of alice/project at url, without tea as credential helper.
func doctorGit(url string) *fakeGit {
return &fakeGit{results: map[string]string{"--version": "git version 2.50.1", "remote": "origin", "remote get-url origin": url + "/alice/project.git"}}
}
func TestEnvironmentCredentialReportsActualAccount(t *testing.T) {
server := newAuthenticatedServer(doctorServer(t))
defer server.Close()
deps, out, _ := testDeps(t)
deps.Git = &fakeGit{results: map[string]string{"--version": "git version 2.50.1"}}
deps.Getenv = serverEnv(server.URL, map[string]string{"TEA_USER": "ALICE"})
require.NoError(t, Execute(context.Background(), deps, []string{"doctor", "--json"}))
require.Contains(t, out.String(), `"user":"alice"`)
require.Contains(t, out.String(), `"user_source":"TEA_USER"`)
}
func TestDoctorDiscoversRepositoryBestEffort(t *testing.T) {
server := newAuthenticatedServer(doctorServer(t))
defer server.Close()
deps, out, _ := serverDeps(t, server.URL)
deps.Git = doctorGit(server.URL)
require.NoError(t, Execute(context.Background(), deps, []string{"doctor", "--json=repo,repo_source"}))
require.JSONEq(t, `{"repo":"alice/project","repo_source":"remote origin"}`, out.String())
}
func TestDoctorReportsEveryCheckInsteadOfFailingFirst(t *testing.T) {
server := newAuthenticatedServer(doctorServer(t))
defer server.Close()
origin := `(git remote "origin")`
for _, tc := range []struct {
name string
env map[string]string
args []string
statuses string
account string
}{
{
name: "environment token", env: map[string]string{"TEA_HOST": server.URL, "GITEA_TOKEN": "token"},
statuses: "Config ok, Instance ok, Account ok, Repository ok, Git warning",
account: "alice (GITEA_TOKEN authenticated account) · credential valid · scopes: all",
},
{
name: "no account for the instance of the remote", env: map[string]string{"TEA_HOST": server.URL},
statuses: "Config ok, Instance ok, Account failed, Repository skipped, Git warning",
account: "no account for " + server.URL + " (instance from " + strings.Trim(origin, "()") + "); run tea auth login --host " + server.URL + ", or use -R or --host to target another instance",
},
{
name: "token of another instance", env: map[string]string{"TEA_HOST": "https://other.example.org", "GITEA_TOKEN": "token"},
statuses: "Config ok, Instance ok, Account failed, Repository skipped, Git warning",
account: "GITEA_TOKEN belongs to https://other.example.org, but the instance here is " + server.URL + " " + origin,
},
{
name: "instance that does not answer", args: []string{"--host", "http://127.0.0.1:1", "-R", "alice/project"},
statuses: "Config ok, Instance failed, Account skipped, Repository skipped, Git warning",
account: "not checked while the instance does not answer",
},
} {
t.Run(tc.name, func(t *testing.T) {
deps, out, _ := testDeps(t)
deps.Git = doctorGit(server.URL)
deps.Getenv = envFunc(tc.env)
err := Execute(context.Background(), deps, append([]string{"doctor", "--json=checks"}, tc.args...))
var report doctorReport
require.NoError(t, json.Unmarshal(out.Bytes(), &report), out.String())
statuses := make([]string, len(report.Checks))
for i, check := range report.Checks {
statuses[i] = check.Name + " " + check.Status
if check.Name == "Account" {
require.Equal(t, tc.account, check.Detail)
}
}
require.Equal(t, tc.statuses, strings.Join(statuses, ", "))
if strings.Contains(tc.statuses, "failed") {
require.Equal(t, clierr.Failure, clierr.ExitCode(err))
require.EqualError(t, err, "1 of 5 checks failed")
} else {
require.NoError(t, err)
}
})
}
}
func TestDoctorShowsChecksOnATerminal(t *testing.T) {
server := newAuthenticatedServer(doctorServer(t))
defer server.Close()
deps, out, _ := testDeps(t)
deps.Streams.OutTTY = true
deps.Git = doctorGit(server.URL)
configureHost(t, deps, server.URL, settings.Host{User: "alice", Users: tokenAccounts(settings.StoreFile, "alice"), SSHHost: "ssh.example.org:2222"})
require.NoError(t, deps.Credentials.Save(server.URL, "alice", settings.StoreFile, auth.Credential{AccessToken: "alice-token"}))
require.NoError(t, Execute(context.Background(), deps, []string{"doctor"}))
lines := withoutANSI(out.String())
for _, want := range []string{
"✓ Config config.yml and hosts.yml in " + deps.Settings.Dir + "\n",
"✓ Account alice (active instance account) · credential valid · scopes: all\n",
"✓ Repository alice/project (git remote \"origin\") · default branch main\n",
"! Git git version 2.50.1 · credential helper not configured for " + server.URL + "\n" +
" → tea auth setup-git, if pushes over HTTPS should use this account\n",
"✓ SSH remotes connect to ssh.example.org:2222 (ssh_host)\n",
} {
require.Contains(t, lines, want)
}
require.Regexp(t, `✓ Instance `+server.URL+` \(git remote "origin"\) · Gitea 28.1.0 · \d+ms\n`, lines)
}