Files
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

337 lines
10 KiB
Go

package forge
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
"time"
"github.com/google/go-github/v88/github"
)
// newTestClient builds a *ghClient whose go-github client points at the
// given test-server base URL. It exercises the real go-github transport;
// only the base URL (and token + slug) are injected.
func newTestClient(t *testing.T, baseURL string) *ghClient {
t.Helper()
base := strings.TrimSuffix(baseURL, "/") + "/"
gh, err := github.NewClient(github.WithAuthToken("test-token"), github.WithURLs(&base, &base))
if err != nil {
t.Fatalf("building github client: %v", err)
}
return &ghClient{gh: gh, owner: "octo", repo: "gortex", timeout: 5 * time.Second}
}
// withTestSeam swaps newClient so the free functions resolve to c, then
// restores it. It returns a cleanup the caller defers.
func withTestSeam(t *testing.T, c *ghClient) {
t.Helper()
prev := newClient
newClient = func(ctx context.Context, repoDir string) (*ghClient, error) { return c, nil }
t.Cleanup(func() { newClient = prev })
}
// fixtureServer routes the recorded GitHub REST endpoints used by forge.
func fixtureServer(t *testing.T) *httptest.Server {
t.Helper()
mux := http.NewServeMux()
mux.HandleFunc("/repos/octo/gortex/pulls", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(prListJSON))
})
mux.HandleFunc("/repos/octo/gortex/pulls/7", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(prGetJSON))
})
mux.HandleFunc("/repos/octo/gortex/pulls/7/files", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(prFilesJSON))
})
mux.HandleFunc("/repos/octo/gortex/pulls/7/reviews", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.Method == http.MethodPost {
// CreateReview
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"id":1,"state":"COMMENTED"}`))
return
}
_, _ = w.Write([]byte(prReviewsJSON))
})
mux.HandleFunc("/repos/octo/gortex/commits/headsha/check-runs", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(checkRunsJSON))
})
mux.HandleFunc("/repos/octo/gortex/commits/headsha/status", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(combinedStatusJSON))
})
srv := httptest.NewServer(mux)
t.Cleanup(srv.Close)
return srv
}
func TestListPRs_FilesEmpty(t *testing.T) {
srv := fixtureServer(t)
c := newTestClient(t, srv.URL)
withTestSeam(t, c)
prs, err := ListPRs(context.Background(), ".", ListOpts{State: "open"})
if err != nil {
t.Fatalf("ListPRs: %v", err)
}
if len(prs) != 1 {
t.Fatalf("got %d PRs, want 1", len(prs))
}
p := prs[0]
if p.Number != 7 {
t.Errorf("Number = %d, want 7", p.Number)
}
if p.Title != "Add forge" {
t.Errorf("Title = %q", p.Title)
}
if p.Author != "alice" {
t.Errorf("Author = %q, want alice", p.Author)
}
if p.BaseRef != "main" || p.HeadRef != "feature" {
t.Errorf("Base/Head = %q/%q", p.BaseRef, p.HeadRef)
}
if len(p.Files) != 0 {
t.Errorf("Files must be EMPTY after ListPRs, got %v", p.Files)
}
// No aggregates requested → unset.
if p.ReviewDecision != "" || p.CIRollup != "" {
t.Errorf("aggregates filled without opt-in: decision=%q ci=%q", p.ReviewDecision, p.CIRollup)
}
}
func TestListPRs_WithDecisionAndCI(t *testing.T) {
srv := fixtureServer(t)
c := newTestClient(t, srv.URL)
withTestSeam(t, c)
prs, err := ListPRs(context.Background(), ".", ListOpts{State: "open", WithDecision: true, WithCI: true})
if err != nil {
t.Fatalf("ListPRs: %v", err)
}
if len(prs) != 1 {
t.Fatalf("got %d PRs, want 1", len(prs))
}
p := prs[0]
if p.ReviewDecision != "CHANGES_REQUESTED" {
t.Errorf("ReviewDecision = %q, want CHANGES_REQUESTED", p.ReviewDecision)
}
if p.CIRollup != "FAILURE" {
t.Errorf("CIRollup = %q, want FAILURE", p.CIRollup)
}
}
func TestViewPR_HydratesFilesAndAggregates(t *testing.T) {
srv := fixtureServer(t)
c := newTestClient(t, srv.URL)
withTestSeam(t, c)
pr, err := ViewPR(context.Background(), ".", 7)
if err != nil {
t.Fatalf("ViewPR: %v", err)
}
if len(pr.Files) != 2 {
t.Fatalf("Files = %v, want 2 entries", pr.Files)
}
if pr.Files[0] != "internal/forge/forge.go" {
t.Errorf("Files[0] = %q", pr.Files[0])
}
if pr.ReviewDecision != "CHANGES_REQUESTED" {
t.Errorf("ReviewDecision = %q", pr.ReviewDecision)
}
if pr.CIRollup != "FAILURE" {
t.Errorf("CIRollup = %q", pr.CIRollup)
}
}
func TestPRFiles(t *testing.T) {
srv := fixtureServer(t)
c := newTestClient(t, srv.URL)
withTestSeam(t, c)
files, err := PRFiles(context.Background(), ".", 7)
if err != nil {
t.Fatalf("PRFiles: %v", err)
}
if len(files) != 2 {
t.Fatalf("got %d files, want 2", len(files))
}
}
func TestDiffPR_HunksParsed(t *testing.T) {
srv := fixtureServer(t)
c := newTestClient(t, srv.URL)
withTestSeam(t, c)
diff, err := DiffPR(context.Background(), ".", 7)
if err != nil {
t.Fatalf("DiffPR: %v", err)
}
if diff.Number != 7 || diff.BaseRef != "main" || diff.HeadRef != "feature" {
t.Errorf("diff meta = %d %q %q", diff.Number, diff.BaseRef, diff.HeadRef)
}
if len(diff.Files) != 2 {
t.Fatalf("got %d files, want 2", len(diff.Files))
}
// First file's patch must parse into at least one hunk anchored to it.
var hunked *PRFile
for i := range diff.Files {
if len(diff.Files[i].Hunks) > 0 {
hunked = &diff.Files[i]
break
}
}
if hunked == nil {
t.Fatalf("no file produced hunks from its patch")
}
if hunked.Hunks[0].FilePath != hunked.Path {
t.Errorf("hunk FilePath = %q, want %q", hunked.Hunks[0].FilePath, hunked.Path)
}
if hunked.Hunks[0].StartLine == 0 {
t.Errorf("hunk StartLine not parsed: %+v", hunked.Hunks[0])
}
if diff.Raw == "" {
t.Errorf("Raw diff is empty")
}
}
func TestPostReviewComments(t *testing.T) {
srv := fixtureServer(t)
c := newTestClient(t, srv.URL)
withTestSeam(t, c)
err := PostReviewComments(context.Background(), ".", 7, []ReviewComment{
{Path: "internal/forge/forge.go", Line: 12, Body: "nit"},
{Path: "internal/forge/ghrest.go", StartLine: 4, Line: 9, Side: "RIGHT", Body: "range"},
})
if err != nil {
t.Fatalf("PostReviewComments: %v", err)
}
}
func TestNewGHClient_NoToken(t *testing.T) {
t.Setenv("GH_TOKEN", "")
t.Setenv("GITHUB_TOKEN", "")
_, err := newGHClient(context.Background(), ".")
if !errors.Is(err, ErrNotAuthenticated) {
t.Fatalf("err = %v, want ErrNotAuthenticated", err)
}
}
func TestAvailable(t *testing.T) {
t.Setenv("GH_TOKEN", "")
t.Setenv("GITHUB_TOKEN", "")
if Available(context.Background()) {
t.Errorf("Available = true with no token")
}
t.Setenv("GITHUB_TOKEN", "tok")
if !Available(context.Background()) {
t.Errorf("Available = false with GITHUB_TOKEN set")
}
}
func TestRateLimited(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/repos/octo/gortex/pulls", func(w http.ResponseWriter, r *http.Request) {
reset := time.Now().Add(42 * time.Second).Unix()
w.Header().Set("X-RateLimit-Limit", "60")
w.Header().Set("X-RateLimit-Remaining", "0")
w.Header().Set("X-RateLimit-Reset", strconv.FormatInt(reset, 10))
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusForbidden)
_, _ = w.Write([]byte(`{"message":"API rate limit exceeded","documentation_url":"https://docs.github.com"}`))
})
srv := httptest.NewServer(mux)
defer srv.Close()
c := newTestClient(t, srv.URL)
withTestSeam(t, c)
_, err := ListPRs(context.Background(), ".", ListOpts{})
if !errors.Is(err, ErrRateLimited) {
t.Fatalf("err = %v, want ErrRateLimited", err)
}
if !strings.Contains(err.Error(), "retry after") {
t.Errorf("rate-limit error missing Retry-After hint: %v", err)
}
}
func TestDefaultBranch_Fallback(t *testing.T) {
// A server with no repos/.../GET handler → API errors → fall back to
// the local probe (which returns "" outside a repo root).
mux := http.NewServeMux()
mux.HandleFunc("/repos/octo/gortex", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte(`{"message":"Not Found"}`))
})
srv := httptest.NewServer(mux)
defer srv.Close()
c := newTestClient(t, srv.URL)
// Should not panic; returns the local-probe fallback ("" here).
_ = c.DefaultBranch(context.Background())
}
func TestDefaultBranch_FromAPI(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/repos/octo/gortex", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"name":"gortex","default_branch":"trunk"}`))
})
srv := httptest.NewServer(mux)
defer srv.Close()
c := newTestClient(t, srv.URL)
if got := c.DefaultBranch(context.Background()); got != "trunk" {
t.Errorf("DefaultBranch = %q, want trunk", got)
}
}
func TestOwnerRepoFrom(t *testing.T) {
tests := []struct {
in, owner, repo string
ok bool
}{
{"github.com/octo/gortex", "octo", "gortex", true},
{"github.com/octo/gortex.git", "octo", "gortex", true},
{"octo/gortex", "octo", "gortex", true},
{"gortex", "", "", false},
{"", "", "", false},
{"ghe.example.com/org/team/repo", "team", "repo", true},
}
for _, tt := range tests {
o, r, ok := ownerRepoFrom(tt.in)
if ok != tt.ok || o != tt.owner || r != tt.repo {
t.Errorf("ownerRepoFrom(%q) = %q,%q,%v; want %q,%q,%v", tt.in, o, r, ok, tt.owner, tt.repo, tt.ok)
}
}
}
func TestEnterpriseBase(t *testing.T) {
t.Setenv("GITHUB_API_URL", "")
t.Setenv("GH_HOST", "")
if got := enterpriseBase(); got != "" {
t.Errorf("enterpriseBase with no env = %q, want empty", got)
}
t.Setenv("GITHUB_API_URL", "https://github.com/api/v3")
if got := enterpriseBase(); got != "" {
t.Errorf("public github.com must not be enterprise: %q", got)
}
t.Setenv("GITHUB_API_URL", "https://ghe.corp.example/api/v3")
if got := enterpriseBase(); got != "https://ghe.corp.example/api/v3/" {
t.Errorf("enterpriseBase = %q", got)
}
t.Setenv("GITHUB_API_URL", "")
t.Setenv("GH_HOST", "ghe.corp.example")
if got := enterpriseBase(); got != "https://ghe.corp.example/api/v3/" {
t.Errorf("enterpriseBase from GH_HOST = %q", got)
}
}