diff --git a/detect.go b/detect.go index 6f0197c..f227281 100644 --- a/detect.go +++ b/detect.go @@ -19,7 +19,10 @@ func DetectForgeType(ctx context.Context, domain string, hc ...*http.Client) (Fo baseURL := "https://" + domain ft, err := detectFromHeaders(ctx, client, baseURL) - if err == nil && ft != Unknown { + if err != nil { + return Unknown, fmt.Errorf("could not detect forge type for %s: %w", baseURL, err) + } + if ft != Unknown { return ft, nil } diff --git a/detect_test.go b/detect_test.go new file mode 100644 index 0000000..5d800fc --- /dev/null +++ b/detect_test.go @@ -0,0 +1,26 @@ +package forges + +import ( + "context" + "errors" + "net/http" + "strings" + "testing" +) + +type errTransport struct{ err error } + +func (t errTransport) RoundTrip(*http.Request) (*http.Response, error) { return nil, t.err } + +func TestDetectForgeTypeSurfacesTransportError(t *testing.T) { + netErr := errors.New("dial tcp: lookup forge.invalid: no such host") + hc := &http.Client{Transport: errTransport{err: netErr}} + + _, err := DetectForgeType(context.Background(), "forge.invalid", hc) + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), netErr.Error()) { + t.Fatalf("expected transport error to be surfaced, got: %v", err) + } +} diff --git a/gitea/branches.go b/gitea/branches.go index 32ea450..316b74b 100644 --- a/gitea/branches.go +++ b/gitea/branches.go @@ -17,10 +17,7 @@ func (f *giteaForge) Branches() forge.BranchService { } func (s *giteaBranchService) List(ctx context.Context, owner, repo string, opts forge.ListBranchOpts) ([]forge.Branch, error) { - perPage := opts.PerPage - if perPage <= 0 { - perPage = 30 - } + perPage := pageSize(opts.PerPage) page := opts.Page if page <= 0 { page = 1 @@ -47,7 +44,7 @@ func (s *giteaBranchService) List(ctx context.Context, owner, repo string, opts } all = append(all, branch) } - if len(branches) < perPage || (opts.Limit > 0 && len(all) >= opts.Limit) { + if lastPage(resp, len(branches), perPage) || (opts.Limit > 0 && len(all) >= opts.Limit) { break } page++ diff --git a/gitea/ci.go b/gitea/ci.go index db2ddd8..2d2d13d 100644 --- a/gitea/ci.go +++ b/gitea/ci.go @@ -70,10 +70,7 @@ func convertGiteaWorkflowJob(j *gitea.ActionWorkflowJob) forge.CIJob { } func (s *giteaCIService) ListRuns(_ context.Context, owner, repo string, opts forge.ListCIRunOpts) ([]forge.CIRun, error) { - perPage := opts.PerPage - if perPage <= 0 { - perPage = 20 - } + perPage := pageSize(opts.PerPage) page := opts.Page if page <= 0 { page = 1 @@ -104,7 +101,7 @@ func (s *giteaCIService) ListRuns(_ context.Context, owner, repo string, opts fo for _, r := range resp.WorkflowRuns { all = append(all, convertGiteaWorkflowRun(r)) } - if len(resp.WorkflowRuns) < perPage || (opts.Limit > 0 && len(all) >= opts.Limit) { + if lastPage(httpResp, len(resp.WorkflowRuns), perPage) || (opts.Limit > 0 && len(all) >= opts.Limit) { break } gOpts.Page++ diff --git a/gitea/collaborators.go b/gitea/collaborators.go index c268110..b902f92 100644 --- a/gitea/collaborators.go +++ b/gitea/collaborators.go @@ -17,10 +17,7 @@ func (f *giteaForge) Collaborators() forge.CollaboratorService { } func (s *giteaCollaboratorService) List(ctx context.Context, owner, repo string, opts forge.ListCollaboratorOpts) ([]forge.Collaborator, error) { - perPage := opts.PerPage - if perPage <= 0 { - perPage = 50 - } + perPage := pageSize(opts.PerPage) page := opts.Page if page <= 0 { page = 1 @@ -47,7 +44,7 @@ func (s *giteaCollaboratorService) List(ctx context.Context, owner, repo string, Permission: perm, }) } - if len(users) < perPage || (opts.Limit > 0 && len(all) >= opts.Limit) { + if lastPage(resp, len(users), perPage) || (opts.Limit > 0 && len(all) >= opts.Limit) { break } page++ diff --git a/gitea/commit_statuses.go b/gitea/commit_statuses.go index 379b10f..2dcf6ee 100644 --- a/gitea/commit_statuses.go +++ b/gitea/commit_statuses.go @@ -10,6 +10,30 @@ import ( const defaultPageSize = 50 +// pageSize caps the requested page size at Gitea's default MAX_RESPONSE_ITEMS. +// Servers clamp larger values, which breaks len(results) < perPage loop exits. +func pageSize(perPage int) int { + if perPage <= 0 || perPage > defaultPageSize { + return defaultPageSize + } + return perPage +} + +// lastPage reports whether a paginated response was the final page. It trusts +// the SDK-parsed Link headers when the server sent any, since Gitea clamps the +// page size to MAX_RESPONSE_ITEMS and a clamped page would otherwise look like +// a short final page. Falls back to the short-page heuristic when no Link +// header was sent. +func lastPage(resp *gitea.Response, got, perPage int) bool { + if got == 0 { + return true + } + if resp != nil && (resp.FirstPage > 0 || resp.PrevPage > 0 || resp.NextPage > 0 || resp.LastPage > 0) { + return resp.NextPage == 0 + } + return got < perPage +} + type giteaCommitStatusService struct { client *gitea.Client } diff --git a/gitea/deploy_keys.go b/gitea/deploy_keys.go index 7e24a52..212f331 100644 --- a/gitea/deploy_keys.go +++ b/gitea/deploy_keys.go @@ -17,10 +17,7 @@ func (f *giteaForge) DeployKeys() forge.DeployKeyService { } func (s *giteaDeployKeyService) List(ctx context.Context, owner, repo string, opts forge.ListDeployKeyOpts) ([]forge.DeployKey, error) { - perPage := opts.PerPage - if perPage <= 0 { - perPage = 30 - } + perPage := pageSize(opts.PerPage) page := opts.Page if page <= 0 { page = 1 @@ -46,7 +43,7 @@ func (s *giteaDeployKeyService) List(ctx context.Context, owner, repo string, op CreatedAt: k.Created, }) } - if len(keys) < perPage || (opts.Limit > 0 && len(all) >= opts.Limit) { + if lastPage(resp, len(keys), perPage) || (opts.Limit > 0 && len(all) >= opts.Limit) { break } page++ diff --git a/gitea/gitea.go b/gitea/gitea.go index b2405d8..0ee8bcb 100644 --- a/gitea/gitea.go +++ b/gitea/gitea.go @@ -100,10 +100,7 @@ func (s *giteaRepoService) Get(ctx context.Context, owner, repo string) (*forge. } func (s *giteaRepoService) List(ctx context.Context, owner string, opts forge.ListRepoOpts) ([]forge.Repository, error) { - perPage := opts.PerPage - if perPage <= 0 { - perPage = defaultPageSize - } + perPage := pageSize(opts.PerPage) // Try org endpoint first, fall back to user on 404. repos, err := s.listOrgRepos(ctx, owner, perPage) @@ -133,7 +130,7 @@ func (s *giteaRepoService) listOrgRepos(_ context.Context, owner string, perPage for _, r := range gRepos { all = append(all, convertGiteaRepo(r)) } - if len(gRepos) < perPage { + if lastPage(resp, len(gRepos), perPage) { break } page++ @@ -157,7 +154,7 @@ func (s *giteaRepoService) listUserRepos(_ context.Context, owner string, perPag for _, r := range gRepos { all = append(all, convertGiteaRepo(r)) } - if len(gRepos) < perPage { + if lastPage(resp, len(gRepos), perPage) { break } page++ @@ -300,10 +297,7 @@ func (s *giteaRepoService) Fork(ctx context.Context, owner, repo string, opts fo } func (s *giteaRepoService) ListForks(ctx context.Context, owner, repo string, opts forge.ListForksOpts) ([]forge.Repository, error) { - perPage := opts.PerPage - if perPage <= 0 { - perPage = defaultPageSize - } + perPage := pageSize(opts.PerPage) page := opts.Page if page <= 0 { page = 1 @@ -323,7 +317,7 @@ func (s *giteaRepoService) ListForks(ctx context.Context, owner, repo string, op for _, r := range forks { all = append(all, convertGiteaRepo(r)) } - if len(forks) < perPage || (opts.Limit > 0 && len(all) >= opts.Limit) { + if lastPage(resp, len(forks), perPage) || (opts.Limit > 0 && len(all) >= opts.Limit) { break } page++ @@ -369,10 +363,7 @@ func (s *giteaRepoService) ListContributors(ctx context.Context, owner, repo str } func (s *giteaRepoService) Search(ctx context.Context, opts forge.SearchRepoOpts) ([]forge.Repository, error) { - perPage := opts.PerPage - if perPage <= 0 { - perPage = 30 - } + perPage := pageSize(opts.PerPage) page := opts.Page if page <= 0 { page = 1 diff --git a/gitea/gitea_test.go b/gitea/gitea_test.go index 4e8c594..9320bdd 100644 --- a/gitea/gitea_test.go +++ b/gitea/gitea_test.go @@ -7,6 +7,7 @@ import ( forge "github.com/git-pkgs/forge" "net/http" "net/http/httptest" + "strings" "testing" "time" ) @@ -198,6 +199,53 @@ func TestGiteaListRepos(t *testing.T) { assertEqual(t, "repos[1].FullName", "testorg/repo-b", repos[1].FullName) } +func TestGiteaListReposPaginatesWhenServerClampsPageSize(t *testing.T) { + const serverCap = 2 + total := 5 + + pages := (total + serverCap - 1) / serverCap + + mux := http.NewServeMux() + mux.HandleFunc("GET /api/v1/version", giteaVersionHandler) + mux.HandleFunc("GET /api/v1/orgs/testorg/repos", func(w http.ResponseWriter, r *http.Request) { + page := 1 + _, _ = fmt.Sscan(r.URL.Query().Get("page"), &page) + + var links []string + links = append(links, `; rel="first"`, fmt.Sprintf(`; rel="last"`, pages)) + if page < pages { + links = append(links, fmt.Sprintf(`; rel="next"`, page+1)) + } + if page > 1 { + links = append(links, fmt.Sprintf(`; rel="prev"`, page-1)) + } + w.Header().Set("Link", strings.Join(links, ", ")) + + start := (page - 1) * serverCap + var out []map[string]any + for i := start; i < start+serverCap && i < total; i++ { + out = append(out, map[string]any{ + "full_name": fmt.Sprintf("testorg/repo-%d", i), + "name": fmt.Sprintf("repo-%d", i), + "owner": map[string]any{"login": "testorg"}, + }) + } + _ = json.NewEncoder(w).Encode(out) + }) + + srv := httptest.NewServer(mux) + defer srv.Close() + + f := New(srv.URL, "", nil) + repos, err := f.Repos().List(context.Background(), "testorg", forge.ListRepoOpts{PerPage: 100}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(repos) != total { + t.Fatalf("expected %d repos across pages, got %d", total, len(repos)) + } +} + func TestGiteaListReposFallbackToUser(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc("GET /api/v1/version", giteaVersionHandler) diff --git a/gitea/issues.go b/gitea/issues.go index 9ef39ac..0ac641a 100644 --- a/gitea/issues.go +++ b/gitea/issues.go @@ -119,10 +119,7 @@ func (s *giteaIssueService) Get(ctx context.Context, owner, repo string, number } func (s *giteaIssueService) List(ctx context.Context, owner, repo string, opts forge.ListIssueOpts) ([]forge.Issue, error) { - perPage := opts.PerPage - if perPage <= 0 { - perPage = 30 - } + perPage := pageSize(opts.PerPage) page := opts.Page if page <= 0 { page = 1 @@ -160,7 +157,7 @@ func (s *giteaIssueService) List(ctx context.Context, owner, repo string, opts f for _, i := range issues { all = append(all, convertGiteaIssue(i)) } - if len(issues) < perPage || (opts.Limit > 0 && len(all) >= opts.Limit) { + if lastPage(resp, len(issues), perPage) || (opts.Limit > 0 && len(all) >= opts.Limit) { break } gOpts.Page++ diff --git a/gitea/labels.go b/gitea/labels.go index 2f4e9fb..0968492 100644 --- a/gitea/labels.go +++ b/gitea/labels.go @@ -27,10 +27,7 @@ func convertGiteaLabel(l *gitea.Label) forge.Label { } func (s *giteaLabelService) List(ctx context.Context, owner, repo string, opts forge.ListLabelOpts) ([]forge.Label, error) { - perPage := opts.PerPage - if perPage <= 0 { - perPage = defaultPageSize - } + perPage := pageSize(opts.PerPage) page := opts.Page if page <= 0 { page = 1 @@ -50,7 +47,7 @@ func (s *giteaLabelService) List(ctx context.Context, owner, repo string, opts f for _, l := range labels { all = append(all, convertGiteaLabel(l)) } - if len(labels) < perPage || (opts.Limit > 0 && len(all) >= opts.Limit) { + if lastPage(resp, len(labels), perPage) || (opts.Limit > 0 && len(all) >= opts.Limit) { break } page++ diff --git a/gitea/milestones.go b/gitea/milestones.go index 92524bf..64a1ba8 100644 --- a/gitea/milestones.go +++ b/gitea/milestones.go @@ -38,10 +38,7 @@ func convertGiteaMilestone(m *gitea.Milestone) forge.Milestone { } func (s *giteaMilestoneService) List(ctx context.Context, owner, repo string, opts forge.ListMilestoneOpts) ([]forge.Milestone, error) { - perPage := opts.PerPage - if perPage <= 0 { - perPage = 30 - } + perPage := pageSize(opts.PerPage) page := opts.Page if page <= 0 { page = 1 @@ -74,7 +71,7 @@ func (s *giteaMilestoneService) List(ctx context.Context, owner, repo string, op for _, m := range milestones { all = append(all, convertGiteaMilestone(m)) } - if len(milestones) < perPage || (opts.Limit > 0 && len(all) >= opts.Limit) { + if lastPage(resp, len(milestones), perPage) || (opts.Limit > 0 && len(all) >= opts.Limit) { break } gOpts.Page++ diff --git a/gitea/notifications.go b/gitea/notifications.go index 798cc04..45e8f62 100644 --- a/gitea/notifications.go +++ b/gitea/notifications.go @@ -57,10 +57,7 @@ func convertGiteaNotification(n *gitea.NotificationThread) forge.Notification { } func (s *giteaNotificationService) List(ctx context.Context, opts forge.ListNotificationOpts) ([]forge.Notification, error) { - perPage := opts.PerPage - if perPage <= 0 { - perPage = 30 - } + perPage := pageSize(opts.PerPage) page := opts.Page if page <= 0 { page = 1 @@ -108,7 +105,7 @@ func (s *giteaNotificationService) listRepoNotifications(owner, repo string, pag for _, n := range notifications { all = append(all, convertGiteaNotification(n)) } - if len(notifications) < perPage || (limit > 0 && len(all) >= limit) { + if lastPage(resp, len(notifications), perPage) || (limit > 0 && len(all) >= limit) { break } page++ @@ -119,7 +116,7 @@ func (s *giteaNotificationService) listRepoNotifications(owner, repo string, pag func (s *giteaNotificationService) listAllNotifications(page, perPage int, statuses []gitea.NotifyStatus, limit int) ([]forge.Notification, error) { var all []forge.Notification for { - notifications, _, err := s.client.ListNotifications(gitea.ListNotificationOptions{ + notifications, resp, err := s.client.ListNotifications(gitea.ListNotificationOptions{ ListOptions: gitea.ListOptions{Page: page, PageSize: perPage}, Status: statuses, }) @@ -129,7 +126,7 @@ func (s *giteaNotificationService) listAllNotifications(page, perPage int, statu for _, n := range notifications { all = append(all, convertGiteaNotification(n)) } - if len(notifications) < perPage || (limit > 0 && len(all) >= limit) { + if lastPage(resp, len(notifications), perPage) || (limit > 0 && len(all) >= limit) { break } page++ diff --git a/gitea/prs.go b/gitea/prs.go index df13ea0..09bf78f 100644 --- a/gitea/prs.go +++ b/gitea/prs.go @@ -122,10 +122,7 @@ func (s *giteaPRService) Get(ctx context.Context, owner, repo string, number int } func (s *giteaPRService) List(ctx context.Context, owner, repo string, opts forge.ListPROpts) ([]forge.PullRequest, error) { - perPage := opts.PerPage - if perPage <= 0 { - perPage = 30 - } + perPage := pageSize(opts.PerPage) page := opts.Page if page <= 0 { page = 1 @@ -162,7 +159,7 @@ func (s *giteaPRService) List(ctx context.Context, owner, repo string, opts forg for _, pr := range prs { all = append(all, convertGiteaPR(pr)) } - if len(prs) < perPage || (opts.Limit > 0 && len(all) >= opts.Limit) { + if lastPage(resp, len(prs), perPage) || (opts.Limit > 0 && len(all) >= opts.Limit) { break } gOpts.Page++ diff --git a/gitea/releases.go b/gitea/releases.go index 6f0fde7..a3842f0 100644 --- a/gitea/releases.go +++ b/gitea/releases.go @@ -57,10 +57,7 @@ func convertGiteaRelease(r *gitea.Release) forge.Release { } func (s *giteaReleaseService) List(ctx context.Context, owner, repo string, opts forge.ListReleaseOpts) ([]forge.Release, error) { - perPage := opts.PerPage - if perPage <= 0 { - perPage = 30 - } + perPage := pageSize(opts.PerPage) page := opts.Page if page <= 0 { page = 1 @@ -80,7 +77,7 @@ func (s *giteaReleaseService) List(ctx context.Context, owner, repo string, opts for _, r := range releases { all = append(all, convertGiteaRelease(r)) } - if len(releases) < perPage || (opts.Limit > 0 && len(all) >= opts.Limit) { + if lastPage(resp, len(releases), perPage) || (opts.Limit > 0 && len(all) >= opts.Limit) { break } page++ diff --git a/gitea/reviews.go b/gitea/reviews.go index da5189b..e4369f5 100644 --- a/gitea/reviews.go +++ b/gitea/reviews.go @@ -58,10 +58,7 @@ func convertGiteaReview(r *gitea.PullReview) forge.Review { } func (s *giteaReviewService) List(ctx context.Context, owner, repo string, number int, opts forge.ListReviewOpts) ([]forge.Review, error) { - perPage := opts.PerPage - if perPage <= 0 { - perPage = 30 - } + perPage := pageSize(opts.PerPage) page := opts.Page if page <= 0 { page = 1 @@ -81,7 +78,7 @@ func (s *giteaReviewService) List(ctx context.Context, owner, repo string, numbe for _, r := range reviews { all = append(all, convertGiteaReview(r)) } - if len(reviews) < perPage || (opts.Limit > 0 && len(all) >= opts.Limit) { + if lastPage(resp, len(reviews), perPage) || (opts.Limit > 0 && len(all) >= opts.Limit) { break } page++ diff --git a/gitea/secrets.go b/gitea/secrets.go index ddfbd45..64bb561 100644 --- a/gitea/secrets.go +++ b/gitea/secrets.go @@ -17,10 +17,7 @@ func (f *giteaForge) Secrets() forge.SecretService { } func (s *giteaSecretService) List(ctx context.Context, owner, repo string, opts forge.ListSecretOpts) ([]forge.Secret, error) { - perPage := opts.PerPage - if perPage <= 0 { - perPage = 30 - } + perPage := pageSize(opts.PerPage) page := opts.Page if page <= 0 { page = 1 @@ -43,7 +40,7 @@ func (s *giteaSecretService) List(ctx context.Context, owner, repo string, opts CreatedAt: sec.Created, }) } - if len(secrets) < perPage || (opts.Limit > 0 && len(all) >= opts.Limit) { + if lastPage(resp, len(secrets), perPage) || (opts.Limit > 0 && len(all) >= opts.Limit) { break } page++ diff --git a/internal/cli/root.go b/internal/cli/root.go index 25d54cd..9f5790b 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -34,6 +34,7 @@ var rootCmd = &cobra.Command{ } resolve.SetRemote(flagRemote) resolve.SetHost(flagHost) + resolve.SetForgeType(flagForgeType) }, } diff --git a/internal/resolve/resolve.go b/internal/resolve/resolve.go index b7d89f1..348e68f 100644 --- a/internal/resolve/resolve.go +++ b/internal/resolve/resolve.go @@ -3,6 +3,7 @@ package resolve import ( "context" "fmt" + "net/http" "os" "os/exec" "strings" @@ -16,8 +17,9 @@ import ( ) var ( - remoteName = "origin" - hostOverride string + remoteName = "origin" + hostOverride string + forgeTypeOverride string ) // SetRemote sets which git remote to read when resolving the current @@ -39,6 +41,15 @@ func SetHost(host string) { } } +// SetForgeType forces the API client implementation for the resolved domain, +// skipping config lookup and network probing. The CLI calls this from the +// --forge-type persistent flag. An empty string is ignored. +func SetForgeType(forgeType string) { + if forgeType != "" { + forgeTypeOverride = forgeType + } +} + var builders = forges.ForgeBuilders{ GitHub: ghforge.NewWithBase, GitLab: glforge.New, @@ -141,20 +152,31 @@ func newClient(domain string) *forges.Client { opts = append(opts, forges.WithForge(d, f)) } - // If the config knows this domain's forge type, register it after defaults - // so it takes precedence. - if ft := configForgeType(domain); ft != "" { - switch ft { - case "gitea", "forgejo": - opts = append(opts, forges.WithForge(domain, gitea.New("https://"+domain, token, hc))) - case "gitlab": - opts = append(opts, forges.WithForge(domain, glforge.New("https://"+domain, token, hc))) - } + // If --forge-type was given or the config knows this domain's type, + // register it after defaults so it takes precedence and probing is skipped. + ft := forgeTypeOverride + if ft == "" { + ft = configForgeType(domain) + } + if f := forgeForType(ft, "https://"+domain, token, hc); f != nil { + opts = append(opts, forges.WithForge(domain, f)) } return forges.NewClient(opts...) } +func forgeForType(forgeType, baseURL, token string, hc *http.Client) forges.Forge { + switch forgeType { + case "gitea", "forgejo": + return gitea.New(baseURL, token, hc) + case "gitlab": + return glforge.New(baseURL, token, hc) + case "github": + return ghforge.NewWithBase(baseURL, token, hc) + } + return nil +} + // forgeForDomainMaybeConfig tries the client's registered forges first. If that // fails and the config declares a type for the domain, it registers the domain // using that type (skipping network detection). Otherwise falls back to probing. @@ -165,7 +187,7 @@ func forgeForDomainMaybeConfig(ctx context.Context, client *forges.Client, domai } token := TokenForDomain(domain) if regErr := client.RegisterDomain(ctx, domain, token, builders); regErr != nil { - return nil, fmt.Errorf("unknown forge at %s: %w", domain, regErr) + return nil, fmt.Errorf("unknown forge at %s: %w (use --forge-type, or set type under [%s] in config, to skip detection)", domain, regErr, domain) } return client.ForgeFor(domain) } diff --git a/internal/resolve/resolve_test.go b/internal/resolve/resolve_test.go index 85f9b5d..7f717d1 100644 --- a/internal/resolve/resolve_test.go +++ b/internal/resolve/resolve_test.go @@ -211,6 +211,39 @@ func TestDomainHostOverride(t *testing.T) { } } +func TestForgeTypeOverrideSkipsDetection(t *testing.T) { + config.ResetCache() + defer config.ResetCache() + t.Setenv("XDG_CONFIG_HOME", t.TempDir()) + + old := forgeTypeOverride + defer func() { forgeTypeOverride = old }() + SetForgeType("gitea") + + f, err := ForgeForDomain("forge.invalid") + if err != nil { + t.Fatalf("--forge-type should skip network detection, got: %v", err) + } + if f == nil { + t.Fatal("expected a forge instance") + } +} + +func TestSetForgeType(t *testing.T) { + old := forgeTypeOverride + defer func() { forgeTypeOverride = old }() + + SetForgeType("gitea") + if forgeTypeOverride != "gitea" { + t.Errorf("SetForgeType did not update forgeTypeOverride, got %q", forgeTypeOverride) + } + + SetForgeType("") + if forgeTypeOverride != "gitea" { + t.Errorf("SetForgeType(\"\") should be a no-op, got %q", forgeTypeOverride) + } +} + func TestSetHost(t *testing.T) { old := hostOverride defer func() { hostOverride = old }()