package forge import ( "errors" "strings" "testing" "github.com/cruldra/superpowers-vscode/cli/internal/repo" "github.com/cruldra/superpowers-vscode/cli/internal/tea" ) // fakeEnv builds an [Env] whose credential sources are canned answers, recording which hosts were asked. type fakeEnv struct { ghToken string ghErr error logins map[string]*tea.TeaLogin env map[string]string ghAsked []string teaAsked []string envAsked []string } func (f *fakeEnv) Env() Env { return Env{ GitHubToken: func(host string) (string, error) { f.ghAsked = append(f.ghAsked, host) return f.ghToken, f.ghErr }, TeaLogin: func(host string) (*tea.TeaLogin, error) { f.teaAsked = append(f.teaAsked, host) return f.logins[host], nil }, Getenv: func(key string) string { f.envAsked = append(f.envAsked, key) return f.env[key] }, } } func githubOrigin() repo.Origin { return repo.Origin{Host: "github.com", WebURL: "https://github.com", Owner: "acme", Repo: "widgets"} } func giteaOrigin() repo.Origin { return repo.Origin{Host: "gitea.example.com", WebURL: "https://gitea.example.com", Owner: "acme", Repo: "widgets"} } func TestKindForHost(t *testing.T) { cases := map[string]Kind{ "github.com": KindGitHub, "GitHub.com": KindGitHub, "gitea.example.com": KindGitea, "github.example.com": KindGitea, "api.github.com": KindGitea, "localhost": KindGitea, } for host, want := range cases { if got := KindForHost(host); got != want { t.Errorf("KindForHost(%q) = %s, want %s", host, got, want) } } } func TestOpenGitHubUsesGhToken(t *testing.T) { fe := &fakeEnv{ghToken: "gho_abc\n", env: map[string]string{"GITEA_TOKEN": "ignored"}} f, err := Open(githubOrigin(), fe.Env()) if err != nil { t.Fatalf("Open 出错: %v", err) } if f.Kind() != KindGitHub { t.Fatalf("github.com 应选 GitHub,得到 %s", f.Kind()) } rf := f.(*restForge) if rf.apiBase != GitHubAPIBaseURL || rf.header.Get("Authorization") != "Bearer gho_abc" { t.Fatalf("GitHub 客户端配置不对: base=%s auth=%q", rf.apiBase, rf.header.Get("Authorization")) } if rf.owner != "acme" || rf.repo != "widgets" { t.Fatalf("owner/repo 不对: %s/%s", rf.owner, rf.repo) } if len(fe.ghAsked) != 1 || fe.ghAsked[0] != "github.com" || len(fe.teaAsked) != 0 { t.Fatalf("应只向 gh 要 github.com 的 token: gh=%v tea=%v", fe.ghAsked, fe.teaAsked) } } func TestOpenGitHubFailsWithLoginHintWhenGhFails(t *testing.T) { fe := &fakeEnv{ghErr: errors.New("exit status 1: no oauth token found")} _, err := Open(githubOrigin(), fe.Env()) if err == nil || !strings.Contains(err.Error(), "gh auth login") { t.Fatalf("gh 取 token 失败应提示运行 gh auth login,得到 %v", err) } if !strings.Contains(err.Error(), "no oauth token found") { t.Fatalf("错误里应带上 gh 的原因: %v", err) } } func TestOpenGitHubFailsWithLoginHintWhenGhTokenEmpty(t *testing.T) { fe := &fakeEnv{ghToken: " \n"} _, err := Open(githubOrigin(), fe.Env()) if err == nil || !strings.Contains(err.Error(), "gh auth login") { t.Fatalf("gh 返回空 token 应提示运行 gh auth login,得到 %v", err) } } func TestOpenGiteaUsesTeaLoginOfOriginHost(t *testing.T) { fe := &fakeEnv{ logins: map[string]*tea.TeaLogin{ "gitea.example.com": {Name: "work", URL: "https://gitea.example.com:8443/", Token: "tea-tok"}, }, env: map[string]string{"GITEA_TOKEN": "env-tok"}, } f, err := Open(giteaOrigin(), fe.Env()) if err != nil { t.Fatalf("Open 出错: %v", err) } rf := f.(*restForge) if f.Kind() != KindGitea || rf.apiBase != "https://gitea.example.com:8443/api/v1" || rf.header.Get("Authorization") != "token tea-tok" { t.Fatalf("Gitea 客户端配置不对: kind=%s base=%s auth=%q", f.Kind(), rf.apiBase, rf.header.Get("Authorization")) } if len(fe.teaAsked) != 1 || fe.teaAsked[0] != "gitea.example.com" || len(fe.ghAsked) != 0 { t.Fatalf("应只按 origin host 查 tea login: tea=%v gh=%v", fe.teaAsked, fe.ghAsked) } } func TestOpenGiteaFallsBackToEnvTokenWhenLoginHasNone(t *testing.T) { fe := &fakeEnv{ logins: map[string]*tea.TeaLogin{ "gitea.example.com": {Name: "work", URL: "https://gitea.example.com", Token: ""}, }, env: map[string]string{"GITEA_TOKEN": "env-tok"}, } f, err := Open(giteaOrigin(), fe.Env()) if err != nil { t.Fatalf("Open 出错: %v", err) } if got := f.(*restForge).header.Get("Authorization"); got != "token env-tok" { t.Fatalf("login 没 token 时应回退 GITEA_TOKEN,得到 %q", got) } } func TestOpenGiteaWithoutLoginForOriginHostNeverReadsEnvToken(t *testing.T) { fe := &fakeEnv{ logins: map[string]*tea.TeaLogin{ "gitea.example.com": {Name: "work", URL: "https://gitea.example.com", Token: "tea-tok"}, }, env: map[string]string{"GITEA_TOKEN": "env-tok"}, } o := repo.Origin{Host: "gitlab.com", WebURL: "https://gitlab.com", Owner: "acme", Repo: "widgets"} f, err := Open(o, fe.Env()) if err == nil || f != nil { t.Fatalf("tea 里没有 origin host 的 login 时应拒绝连接,得到 forge=%v err=%v", f, err) } if !strings.Contains(err.Error(), "tea login add") || !strings.Contains(err.Error(), "gitlab.com") { t.Fatalf("错误应点名 host 并提示先 tea login add,得到 %v", err) } if len(fe.envAsked) != 0 { t.Fatalf("没有同 host 的 login 时不能读 GITEA_TOKEN,免得把它发给别的网站: %v", fe.envAsked) } } func TestOpenGiteaFailsWhenLoginAndEnvHaveNoToken(t *testing.T) { fe := &fakeEnv{ logins: map[string]*tea.TeaLogin{ "gitea.example.com": {Name: "work", URL: "https://gitea.example.com", Token: ""}, }, } _, err := Open(giteaOrigin(), fe.Env()) if err == nil || !strings.Contains(err.Error(), "GITEA_TOKEN") || !strings.Contains(err.Error(), "gitea.example.com") { t.Fatalf("login 和 GITEA_TOKEN 都没有 token 时应报错并点名 host 与 GITEA_TOKEN,得到 %v", err) } }