package forge import ( "encoding/json" "fmt" "net/http" "net/http/httptest" "strings" "sync/atomic" "testing" ) func TestNextLink(t *testing.T) { cases := map[string]string{ ``: ``, `; rel="next", ; rel="last"`: `https://api.github.com/repositories/1/issues?page=2`, `; rel="prev", ; rel="next"`: `https://x/a?page=3`, `; rel="first", ; rel="prev"`: ``, `; rel=next`: `https://x/a?labels=a,b&page=2`, `; rel="next",; rel="last"`: `http://localhost/api/v1/repos/search?limit=1&page=2`, } for header, want := range cases { if got := nextLink(header); got != want { t.Errorf("nextLink(%q) = %q, want %q", header, got, want) } } } // pagedServer serves `total` comments `size` at a time at any path, linking each page to the next one under `nextPath`. func pagedServer(t *testing.T, total, size int, nextPath string, check func(r *http.Request)) (*httptest.Server, *int32) { t.Helper() var calls int32 var srv *httptest.Server srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { atomic.AddInt32(&calls, 1) check(r) page := 1 fmt.Sscan(r.URL.Query().Get("page"), &page) start := (page - 1) * size end := min(start+size, total) if end < total { w.Header().Set("Link", fmt.Sprintf(`<%s%s?page=%d>; rel="next"`, srv.URL, nextPath, page+1)) } out := []map[string]any{} for i := start; i < end; i++ { out = append(out, map[string]any{"id": i + 1, "body": fmt.Sprintf("c%d", i+1), "user": map[string]any{"login": "bob"}}) } _ = json.NewEncoder(w).Encode(out) })) t.Cleanup(srv.Close) return srv, &calls } func TestGitHubPaginationFollowsLinkToOtherPath(t *testing.T) { srv, calls := pagedServer(t, 230, 100, "/repositories/42/issues/7/comments", func(r *http.Request) { if r.Header.Get("Authorization") != "Bearer tok" || r.Header.Get("Accept") != "application/vnd.github+json" || r.Header.Get("X-GitHub-Api-Version") != "2022-11-28" { t.Errorf("GitHub 请求头不对: %v", r.Header) } if r.URL.Query().Get("page") == "" { if r.URL.Path != "/repos/o/r/issues/7/comments" || r.URL.Query().Get("per_page") != "100" { t.Errorf("第一页请求不对: %s", r.URL.RequestURI()) } } else if r.URL.Path != "/repositories/42/issues/7/comments" { t.Errorf("后续页应照 Link 走: %s", r.URL.RequestURI()) } }) list, err := NewGitHub(srv.URL, "tok", "o", "r").ListIssueComments(7) if err != nil { t.Fatalf("ListIssueComments 出错: %v", err) } if len(list) != 230 || list[229].Body != "c230" || *calls != 3 { t.Fatalf("应 3 次请求取全 230 条,得到 %d 条 / %d 次", len(list), *calls) } } func TestGiteaPaginationFollowsLinkWithLimit(t *testing.T) { srv, calls := pagedServer(t, 120, 50, "/api/v1/repos/o/r/issues/7/comments", func(r *http.Request) { if r.Header.Get("Authorization") != "token tok" { t.Errorf("Gitea 认证头不对: %q", r.Header.Get("Authorization")) } if !strings.HasPrefix(r.URL.Path, "/api/v1/") { t.Errorf("Gitea 请求应在 /api/v1 下: %s", r.URL.Path) } if r.URL.Query().Get("page") == "" { if r.URL.Query().Get("limit") != "50" { t.Errorf("第一页应带 limit=50: %s", r.URL.RequestURI()) } } }) list, err := NewGitea(srv.URL, "tok", "o", "r").ListIssueComments(7) if err != nil { t.Fatalf("ListIssueComments 出错: %v", err) } if len(list) != 120 || *calls != 3 { t.Fatalf("应 3 次请求取全 120 条,得到 %d 条 / %d 次", len(list), *calls) } } func TestPaginationStopsWithoutLinkHeader(t *testing.T) { srv, calls := pagedServer(t, 120, 1000, "", func(*http.Request) {}) list, err := NewGitea(srv.URL, "tok", "o", "r").ListIssueComments(7) if err != nil { t.Fatalf("ListIssueComments 出错: %v", err) } if len(list) != 120 || *calls != 1 { t.Fatalf("没有 Link 头时应一次取完,得到 %d 条 / %d 次", len(list), *calls) } } func TestPaginationRefusesLinkToAnotherHost(t *testing.T) { var foreignCalls int32 foreign := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { atomic.AddInt32(&foreignCalls, 1) _, _ = w.Write([]byte("[]")) })) t.Cleanup(foreign.Close) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Link", fmt.Sprintf(`<%s/steal?page=2>; rel="next"`, foreign.URL)) _, _ = w.Write([]byte(`[{"id":1,"body":"x","user":{"login":"bob"}}]`)) })) t.Cleanup(srv.Close) _, err := NewGitHub(srv.URL, "tok", "o", "r").ListIssueComments(7) if err == nil { t.Fatal("Link 指向别的主机时应报错") } if foreignCalls != 0 { t.Fatalf("不应带 token 请求别的主机,实际请求了 %d 次", foreignCalls) } }