package forge_test import ( "fmt" "reflect" "strings" "testing" "github.com/cruldra/superpowers-vscode/cli/internal/forge" "github.com/cruldra/superpowers-vscode/cli/internal/forge/forgetest" ) // flavors runs fn once against a fake Gitea and once against a fake GitHub, each wired to a matching client. func flavors(t *testing.T, fn func(t *testing.T, srv *forgetest.Server, f forge.Forge)) { cases := []struct { name string flavor forgetest.Flavor }{ {"gitea", forgetest.Gitea}, {"github", forgetest.GitHub}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { srv := forgetest.NewServer(t, c.flavor) fn(t, srv, clientFor(srv)) }) } } func clientFor(srv *forgetest.Server) forge.Forge { if srv.Flavor == forgetest.GitHub { return forge.NewGitHub(srv.URL, srv.Token, forgetest.Owner, forgetest.Repo) } return forge.NewGitea(srv.URL, srv.Token, forgetest.Owner, forgetest.Repo) } func TestKindMatchesConstructor(t *testing.T) { if k := forge.NewGitea("https://g.example.com", "t", "o", "r").Kind(); k != forge.KindGitea { t.Fatalf("NewGitea().Kind() = %s", k) } if k := forge.NewGitHub(forge.GitHubAPIBaseURL, "t", "o", "r").Kind(); k != forge.KindGitHub { t.Fatalf("NewGitHub().Kind() = %s", k) } } func TestCurrentUserLogin(t *testing.T) { flavors(t, func(t *testing.T, srv *forgetest.Server, f forge.Forge) { login, err := f.CurrentUserLogin() if err != nil || login != "alice" { t.Fatalf("CurrentUserLogin() = (%q, %v)", login, err) } }) } func TestWrongTokenSurfacesHTTPError(t *testing.T) { flavors(t, func(t *testing.T, srv *forgetest.Server, _ forge.Forge) { var f forge.Forge if srv.Flavor == forgetest.GitHub { f = forge.NewGitHub(srv.URL, "wrong", forgetest.Owner, forgetest.Repo) } else { f = forge.NewGitea(srv.URL, "wrong", forgetest.Owner, forgetest.Repo) } _, err := f.CurrentUserLogin() if err == nil || !strings.Contains(err.Error(), "401") { t.Fatalf("错误 token 应返回带 401 的错误,得到 %v", err) } }) } func TestCreateGetAndUpdateIssue(t *testing.T) { flavors(t, func(t *testing.T, srv *forgetest.Server, f forge.Forge) { created, err := f.CreateIssue("标题", "正文", []string{"alice"}) if err != nil { t.Fatalf("CreateIssue 出错: %v", err) } if created.Number <= 0 || created.HTMLURL == "" { t.Fatalf("CreateIssue 返回不完整: %+v", created) } if err := f.UpdateIssueBody(created.Number, "新正文"); err != nil { t.Fatalf("UpdateIssueBody 出错: %v", err) } got, err := f.GetIssue(created.Number) if err != nil { t.Fatalf("GetIssue 出错: %v", err) } want := &forge.Issue{ Number: created.Number, Title: "标题", Body: "新正文", State: "open", Author: "alice", Assignees: []string{"alice"}, HTMLURL: fmt.Sprintf("%s/acme/widgets/issues/%d", srv.URL, created.Number), } if !reflect.DeepEqual(got, want) { t.Fatalf("GetIssue = %+v\nwant %+v", got, want) } }) } func TestGetIssueWithoutAssigneesGivesEmptyList(t *testing.T) { flavors(t, func(t *testing.T, srv *forgetest.Server, f forge.Forge) { n := srv.AddIssue(forgetest.Issue{Title: "t", Author: "bob"}) got, err := f.GetIssue(n) if err != nil { t.Fatalf("GetIssue 出错: %v", err) } if got.Assignees == nil || len(got.Assignees) != 0 { t.Fatalf("没有指派人时应是非 nil 空切片,得到 %#v", got.Assignees) } }) } func TestCommentsRoundTrip(t *testing.T) { flavors(t, func(t *testing.T, srv *forgetest.Server, f forge.Forge) { n := srv.AddIssue(forgetest.Issue{Title: "t", Author: "bob"}) srv.AddComment(n, "bob", "第一条") posted, err := f.CreateIssueComment(n, "第二条") if err != nil { t.Fatalf("CreateIssueComment 出错: %v", err) } if posted.ID == 0 || posted.HTMLURL == "" || posted.Author != "alice" { t.Fatalf("CreateIssueComment 返回不完整: %+v", posted) } list, err := f.ListIssueComments(n) if err != nil { t.Fatalf("ListIssueComments 出错: %v", err) } if len(list) != 2 || list[0].Body != "第一条" || list[0].Author != "bob" || list[1].Body != "第二条" { t.Fatalf("评论列表不对: %+v", list) } if list[0].CreatedAt == "" { t.Fatalf("评论应带 created_at: %+v", list[0]) } }) } func TestListIssueCommentsReadsEveryPageOnGitHub(t *testing.T) { srv := forgetest.NewServer(t, forgetest.GitHub) f := clientFor(srv) n := srv.AddIssue(forgetest.Issue{Title: "t", Author: "bob"}) for i := 1; i <= 250; i++ { srv.AddComment(n, "bob", fmt.Sprintf("c%d", i)) } list, err := f.ListIssueComments(n) if err != nil { t.Fatalf("ListIssueComments 出错: %v", err) } if len(list) != 250 || list[0].Body != "c1" || list[249].Body != "c250" { t.Fatalf("应读全 250 条且保持顺序,得到 %d 条", len(list)) } var commentCalls []string for _, r := range srv.Requests() { if strings.Contains(r, "/comments") { commentCalls = append(commentCalls, r) } } if len(commentCalls) != 3 { t.Fatalf("250 条应按 per_page=100 分 3 页取,实际请求: %v", commentCalls) } if !strings.Contains(commentCalls[0], "per_page=100") { t.Fatalf("第一页应带 per_page=100: %s", commentCalls[0]) } } func TestGetPullRequest(t *testing.T) { flavors(t, func(t *testing.T, srv *forgetest.Server, f forge.Forge) { n := srv.AddPull(forgetest.Pull{ Title: "加功能", Body: "说明", State: "closed", Merged: true, Author: "bob", HeadBranch: "feat/x", HeadSHA: "aaa111", BaseBranch: "main", BaseSHA: "bbb222", }) got, err := f.GetPullRequest(n) if err != nil { t.Fatalf("GetPullRequest 出错: %v", err) } if got.Number != n || got.Title != "加功能" || got.Body != "说明" || got.State != "closed" || !got.Merged || got.Author != "bob" { t.Fatalf("PR 字段不对: %+v", got) } if got.Head != (forge.BranchRef{Branch: "feat/x", SHA: "aaa111"}) || got.Base != (forge.BranchRef{Branch: "main", SHA: "bbb222"}) { t.Fatalf("head/base 不对: %+v / %+v", got.Head, got.Base) } if got.HTMLURL == "" { t.Fatal("PR 应带 html_url") } }) } func TestGetPullRequestReportsHeadBranchAfterItIsDeleted(t *testing.T) { flavors(t, func(t *testing.T, srv *forgetest.Server, f forge.Forge) { n := srv.AddPull(forgetest.Pull{ Title: "t", State: "closed", Merged: true, Author: "bob", HeadBranch: "feat/gone", HeadSHA: "aaa111", BaseBranch: "main", BaseSHA: "bbb222", HeadDeleted: true, }) got, err := f.GetPullRequest(n) if err != nil { t.Fatalf("GetPullRequest 出错: %v", err) } if got.Head.Branch != "feat/gone" || got.Base.Branch != "main" { t.Fatalf("head 分支删掉后仍应报分支名,得到 head=%+v base=%+v", got.Head, got.Base) } }) } func TestCreatePullRequestAndDefaultBranch(t *testing.T) { flavors(t, func(t *testing.T, srv *forgetest.Server, f forge.Forge) { srv.DefaultBranch = "trunk" base, err := f.DefaultBranch() if err != nil || base != "trunk" { t.Fatalf("DefaultBranch() = (%q, %v)", base, err) } pr, err := f.CreatePullRequest(forge.NewPullRequest{Title: "T", Body: "B", Head: "feat/y", Base: base}) if err != nil { t.Fatalf("CreatePullRequest 出错: %v", err) } stored, ok := srv.Pull(pr.Number) if !ok || stored.Title != "T" || stored.Body != "B" || stored.HeadBranch != "feat/y" || stored.BaseBranch != "trunk" { t.Fatalf("服务端收到的 PR 不对: %+v", stored) } if pr.HTMLURL == "" || pr.Head.Branch != "feat/y" || pr.Base.Branch != "trunk" { t.Fatalf("CreatePullRequest 返回不完整: %+v", pr) } }) } func TestPullRequestCommentsUseIssueCommentsEndpoint(t *testing.T) { flavors(t, func(t *testing.T, srv *forgetest.Server, f forge.Forge) { n := srv.AddPull(forgetest.Pull{Title: "t", Author: "bob", HeadBranch: "h", BaseBranch: "main"}) if _, err := f.CreateIssueComment(n, "看过了"); err != nil { t.Fatalf("CreateIssueComment 出错: %v", err) } reqs := srv.Requests() prefix := "" if srv.Flavor == forgetest.Gitea { prefix = "/api/v1" } want := fmt.Sprintf("POST %s/repos/acme/widgets/issues/%d/comments", prefix, n) if reqs[len(reqs)-1] != want { t.Fatalf("PR 评论应走 issues comments 接口,实际 %s,want %s", reqs[len(reqs)-1], want) } }) }