test: add unit tests for ListOrgsAndRepos, CreateIssue, ListAllIssues, ListAllPullRequests
Add comprehensive unit tests using mock HTTP servers for four key aggregation methods in the Gitea client. Tests cover correct API integration, caching behavior, sorting, state filtering, repo filtering, pagination, and label handling. Closes leeworks-agents/gitea-mobile#122 Closes leeworks-agents/gitea-mobile#121 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ package gitea
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -374,3 +375,580 @@ func sortTriageQueue(queue []TriageItem) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Issue #122: Tests for ListOrgsAndRepos and CreateIssue ---
|
||||||
|
|
||||||
|
func TestListOrgsAndRepos(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/api/v1/user/orgs":
|
||||||
|
orgs := []Org{
|
||||||
|
{Name: "org1", FullName: "Organization 1"},
|
||||||
|
{Name: "org2", FullName: "Organization 2"},
|
||||||
|
}
|
||||||
|
json.NewEncoder(w).Encode(orgs)
|
||||||
|
case "/api/v1/orgs/org1/repos":
|
||||||
|
repos := []Repo{
|
||||||
|
{ID: 1, Name: "repo-a", FullName: "org1/repo-a"},
|
||||||
|
{ID: 2, Name: "repo-b", FullName: "org1/repo-b"},
|
||||||
|
}
|
||||||
|
json.NewEncoder(w).Encode(repos)
|
||||||
|
case "/api/v1/orgs/org2/repos":
|
||||||
|
repos := []Repo{
|
||||||
|
{ID: 3, Name: "repo-c", FullName: "org2/repo-c"},
|
||||||
|
}
|
||||||
|
json.NewEncoder(w).Encode(repos)
|
||||||
|
default:
|
||||||
|
t.Errorf("unexpected request path: %s", r.URL.Path)
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
c := NewClient(server.URL)
|
||||||
|
result, err := c.ListOrgsAndRepos(context.Background(), "test-token")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result) != 2 {
|
||||||
|
t.Fatalf("got %d orgs, want 2", len(result))
|
||||||
|
}
|
||||||
|
if len(result["org1"]) != 2 {
|
||||||
|
t.Errorf("org1 has %d repos, want 2", len(result["org1"]))
|
||||||
|
}
|
||||||
|
if len(result["org2"]) != 1 {
|
||||||
|
t.Errorf("org2 has %d repos, want 1", len(result["org2"]))
|
||||||
|
}
|
||||||
|
if result["org1"][0].Name != "repo-a" {
|
||||||
|
t.Errorf("org1 repos[0].Name = %q, want %q", result["org1"][0].Name, "repo-a")
|
||||||
|
}
|
||||||
|
if result["org2"][0].Name != "repo-c" {
|
||||||
|
t.Errorf("org2 repos[0].Name = %q, want %q", result["org2"][0].Name, "repo-c")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListOrgsAndRepos_Cached(t *testing.T) {
|
||||||
|
orgCallCount := 0
|
||||||
|
repoCallCount := 0
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/api/v1/user/orgs":
|
||||||
|
orgCallCount++
|
||||||
|
json.NewEncoder(w).Encode([]Org{{Name: "org1"}})
|
||||||
|
case "/api/v1/orgs/org1/repos":
|
||||||
|
repoCallCount++
|
||||||
|
json.NewEncoder(w).Encode([]Repo{{ID: 1, Name: "repo1", FullName: "org1/repo1"}})
|
||||||
|
default:
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
c := NewClient(server.URL)
|
||||||
|
|
||||||
|
// First call populates cache.
|
||||||
|
_, err := c.ListOrgsAndRepos(context.Background(), "test-token")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("first call: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second call should use cached orgs and repos (ListOrgs and ListOrgRepos both cache).
|
||||||
|
_, err = c.ListOrgsAndRepos(context.Background(), "test-token")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("second call: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if orgCallCount != 1 {
|
||||||
|
t.Errorf("org endpoint called %d times, want 1 (cached)", orgCallCount)
|
||||||
|
}
|
||||||
|
if repoCallCount != 1 {
|
||||||
|
t.Errorf("repo endpoint called %d times, want 1 (cached)", repoCallCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateIssue(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
t.Errorf("expected POST, got %s", r.Method)
|
||||||
|
}
|
||||||
|
if r.URL.Path != "/api/v1/repos/owner1/repo1/issues" {
|
||||||
|
t.Errorf("unexpected path: %s", r.URL.Path)
|
||||||
|
}
|
||||||
|
if r.Header.Get("Authorization") != "token test-token" {
|
||||||
|
t.Error("missing or wrong Authorization header")
|
||||||
|
}
|
||||||
|
|
||||||
|
var body map[string]interface{}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
t.Fatalf("failed to decode body: %v", err)
|
||||||
|
}
|
||||||
|
if body["title"] != "Test Issue" {
|
||||||
|
t.Errorf("expected title='Test Issue', got %q", body["title"])
|
||||||
|
}
|
||||||
|
if body["body"] != "Issue body here" {
|
||||||
|
t.Errorf("expected body='Issue body here', got %q", body["body"])
|
||||||
|
}
|
||||||
|
|
||||||
|
issue := map[string]interface{}{
|
||||||
|
"id": 1,
|
||||||
|
"number": 42,
|
||||||
|
"title": body["title"],
|
||||||
|
"body": body["body"],
|
||||||
|
"state": "open",
|
||||||
|
"created_at": "2026-03-28T00:00:00Z",
|
||||||
|
"updated_at": "2026-03-28T00:00:00Z",
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
json.NewEncoder(w).Encode(issue)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
c := NewClient(server.URL)
|
||||||
|
// Pre-populate cache to verify invalidation.
|
||||||
|
c.setCache("issues-org1", "should-be-invalidated")
|
||||||
|
|
||||||
|
issue, err := c.CreateIssue(context.Background(), "test-token", "owner1", "repo1", "Test Issue", "Issue body here", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if issue.Title != "Test Issue" {
|
||||||
|
t.Errorf("issue.Title = %q, want %q", issue.Title, "Test Issue")
|
||||||
|
}
|
||||||
|
if issue.Number != 42 {
|
||||||
|
t.Errorf("issue.Number = %d, want 42", issue.Number)
|
||||||
|
}
|
||||||
|
if issue.RepoOwner != "owner1" {
|
||||||
|
t.Errorf("issue.RepoOwner = %q, want %q", issue.RepoOwner, "owner1")
|
||||||
|
}
|
||||||
|
if issue.RepoName != "repo1" {
|
||||||
|
t.Errorf("issue.RepoName = %q, want %q", issue.RepoName, "repo1")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify cache was invalidated.
|
||||||
|
_, ok := c.getFromCache("issues-org1")
|
||||||
|
if ok {
|
||||||
|
t.Error("expected cache to be invalidated after CreateIssue")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateIssue_WithLabels(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var body map[string]interface{}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
t.Fatalf("failed to decode body: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
labels, ok := body["labels"]
|
||||||
|
if !ok {
|
||||||
|
t.Error("expected labels in request body")
|
||||||
|
}
|
||||||
|
labelSlice, ok := labels.([]interface{})
|
||||||
|
if !ok || len(labelSlice) != 2 {
|
||||||
|
t.Errorf("expected 2 labels, got %v", labels)
|
||||||
|
}
|
||||||
|
|
||||||
|
issue := map[string]interface{}{
|
||||||
|
"id": 2,
|
||||||
|
"number": 43,
|
||||||
|
"title": body["title"],
|
||||||
|
"body": body["body"],
|
||||||
|
"state": "open",
|
||||||
|
"created_at": "2026-03-28T00:00:00Z",
|
||||||
|
"updated_at": "2026-03-28T00:00:00Z",
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
json.NewEncoder(w).Encode(issue)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
c := NewClient(server.URL)
|
||||||
|
issue, err := c.CreateIssue(context.Background(), "test-token", "owner1", "repo1", "Labeled Issue", "body", []int64{10, 20})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if issue.Number != 43 {
|
||||||
|
t.Errorf("issue.Number = %d, want 43", issue.Number)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Issue #121: Tests for ListAllIssues and ListAllPullRequests ---
|
||||||
|
|
||||||
|
// newFanOutServer creates a mock HTTP server that serves orgs, repos, issues, and PRs
|
||||||
|
// for testing the fan-out aggregation functions.
|
||||||
|
func newFanOutServer(t *testing.T) *httptest.Server {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
now := time.Date(2026, 3, 28, 12, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch {
|
||||||
|
case r.URL.Path == "/api/v1/orgs/org1/repos":
|
||||||
|
repos := []Repo{
|
||||||
|
{ID: 1, Name: "repo-a", FullName: "org1/repo-a", Owner: struct {
|
||||||
|
Login string `json:"login"`
|
||||||
|
}{Login: "org1"}},
|
||||||
|
{ID: 2, Name: "repo-b", FullName: "org1/repo-b", Owner: struct {
|
||||||
|
Login string `json:"login"`
|
||||||
|
}{Login: "org1"}},
|
||||||
|
}
|
||||||
|
json.NewEncoder(w).Encode(repos)
|
||||||
|
|
||||||
|
case r.URL.Path == "/api/v1/repos/org1/repo-a/issues":
|
||||||
|
issues := []Issue{
|
||||||
|
{ID: 1, Number: 1, Title: "Issue A1", State: "open", UpdatedAt: now.Add(-1 * time.Hour)},
|
||||||
|
{ID: 2, Number: 2, Title: "Issue A2", State: "open", UpdatedAt: now.Add(-3 * time.Hour)},
|
||||||
|
}
|
||||||
|
json.NewEncoder(w).Encode(issues)
|
||||||
|
|
||||||
|
case r.URL.Path == "/api/v1/repos/org1/repo-b/issues":
|
||||||
|
issues := []Issue{
|
||||||
|
{ID: 3, Number: 1, Title: "Issue B1", State: "open", UpdatedAt: now.Add(-2 * time.Hour)},
|
||||||
|
}
|
||||||
|
json.NewEncoder(w).Encode(issues)
|
||||||
|
|
||||||
|
case r.URL.Path == "/api/v1/repos/org1/repo-a/pulls":
|
||||||
|
prs := []PullRequest{
|
||||||
|
{ID: 10, Number: 5, Title: "PR A1", State: "open", UpdatedAt: now.Add(-30 * time.Minute)},
|
||||||
|
}
|
||||||
|
json.NewEncoder(w).Encode(prs)
|
||||||
|
|
||||||
|
case r.URL.Path == "/api/v1/repos/org1/repo-b/pulls":
|
||||||
|
prs := []PullRequest{
|
||||||
|
{ID: 11, Number: 6, Title: "PR B1", State: "open", UpdatedAt: now.Add(-10 * time.Minute)},
|
||||||
|
{ID: 12, Number: 7, Title: "PR B2", State: "open", UpdatedAt: now.Add(-1 * time.Hour)},
|
||||||
|
}
|
||||||
|
json.NewEncoder(w).Encode(prs)
|
||||||
|
|
||||||
|
default:
|
||||||
|
t.Errorf("unexpected request path: %s", r.URL.Path)
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListAllIssues_Sorting(t *testing.T) {
|
||||||
|
server := newFanOutServer(t)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
c := NewClient(server.URL)
|
||||||
|
// Pre-populate org repos cache to avoid needing the /user/orgs endpoint.
|
||||||
|
c.setCache("repos-org1", []Repo{
|
||||||
|
{ID: 1, Name: "repo-a", FullName: "org1/repo-a", Owner: struct {
|
||||||
|
Login string `json:"login"`
|
||||||
|
}{Login: "org1"}},
|
||||||
|
{ID: 2, Name: "repo-b", FullName: "org1/repo-b", Owner: struct {
|
||||||
|
Login string `json:"login"`
|
||||||
|
}{Login: "org1"}},
|
||||||
|
})
|
||||||
|
|
||||||
|
result, err := c.ListAllIssues(context.Background(), "test-token", []string{"org1"}, "open", 1, "", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result.Issues) != 3 {
|
||||||
|
t.Fatalf("got %d issues, want 3", len(result.Issues))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should be sorted by UpdatedAt descending (newest first).
|
||||||
|
// Issue A1 (-1h), Issue B1 (-2h), Issue A2 (-3h).
|
||||||
|
expected := []string{"Issue A1", "Issue B1", "Issue A2"}
|
||||||
|
for i, title := range expected {
|
||||||
|
if result.Issues[i].Title != title {
|
||||||
|
t.Errorf("issues[%d].Title = %q, want %q", i, result.Issues[i].Title, title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListAllIssues_StateFilter(t *testing.T) {
|
||||||
|
stateReceived := ""
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch {
|
||||||
|
case r.URL.Path == "/api/v1/orgs/org1/repos":
|
||||||
|
repos := []Repo{
|
||||||
|
{ID: 1, Name: "repo-a", FullName: "org1/repo-a", Owner: struct {
|
||||||
|
Login string `json:"login"`
|
||||||
|
}{Login: "org1"}},
|
||||||
|
}
|
||||||
|
json.NewEncoder(w).Encode(repos)
|
||||||
|
case r.URL.Path == "/api/v1/repos/org1/repo-a/issues":
|
||||||
|
stateReceived = r.URL.Query().Get("state")
|
||||||
|
json.NewEncoder(w).Encode([]Issue{})
|
||||||
|
default:
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
c := NewClient(server.URL)
|
||||||
|
c.setCache("repos-org1", []Repo{
|
||||||
|
{ID: 1, Name: "repo-a", FullName: "org1/repo-a", Owner: struct {
|
||||||
|
Login string `json:"login"`
|
||||||
|
}{Login: "org1"}},
|
||||||
|
})
|
||||||
|
|
||||||
|
_, err := c.ListAllIssues(context.Background(), "test-token", []string{"org1"}, "closed", 1, "", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if stateReceived != "closed" {
|
||||||
|
t.Errorf("state query param = %q, want %q", stateReceived, "closed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListAllIssues_DefaultState(t *testing.T) {
|
||||||
|
stateReceived := ""
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch {
|
||||||
|
case r.URL.Path == "/api/v1/orgs/org1/repos":
|
||||||
|
json.NewEncoder(w).Encode([]Repo{
|
||||||
|
{ID: 1, Name: "repo-a", FullName: "org1/repo-a", Owner: struct {
|
||||||
|
Login string `json:"login"`
|
||||||
|
}{Login: "org1"}},
|
||||||
|
})
|
||||||
|
case r.URL.Path == "/api/v1/repos/org1/repo-a/issues":
|
||||||
|
stateReceived = r.URL.Query().Get("state")
|
||||||
|
json.NewEncoder(w).Encode([]Issue{})
|
||||||
|
default:
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
c := NewClient(server.URL)
|
||||||
|
c.setCache("repos-org1", []Repo{
|
||||||
|
{ID: 1, Name: "repo-a", FullName: "org1/repo-a", Owner: struct {
|
||||||
|
Login string `json:"login"`
|
||||||
|
}{Login: "org1"}},
|
||||||
|
})
|
||||||
|
|
||||||
|
_, err := c.ListAllIssues(context.Background(), "test-token", []string{"org1"}, "", 1, "", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if stateReceived != "open" {
|
||||||
|
t.Errorf("default state = %q, want %q", stateReceived, "open")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListAllIssues_RepoFilter(t *testing.T) {
|
||||||
|
server := newFanOutServer(t)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
c := NewClient(server.URL)
|
||||||
|
c.setCache("repos-org1", []Repo{
|
||||||
|
{ID: 1, Name: "repo-a", FullName: "org1/repo-a", Owner: struct {
|
||||||
|
Login string `json:"login"`
|
||||||
|
}{Login: "org1"}},
|
||||||
|
{ID: 2, Name: "repo-b", FullName: "org1/repo-b", Owner: struct {
|
||||||
|
Login string `json:"login"`
|
||||||
|
}{Login: "org1"}},
|
||||||
|
})
|
||||||
|
|
||||||
|
result, err := c.ListAllIssues(context.Background(), "test-token", []string{"org1"}, "open", 1, "", "repo-a")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only issues from repo-a should be returned.
|
||||||
|
if len(result.Issues) != 2 {
|
||||||
|
t.Fatalf("got %d issues, want 2 (only from repo-a)", len(result.Issues))
|
||||||
|
}
|
||||||
|
for _, issue := range result.Issues {
|
||||||
|
if issue.RepoName != "repo-a" {
|
||||||
|
t.Errorf("issue %q has RepoName=%q, want repo-a", issue.Title, issue.RepoName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListAllIssues_Pagination(t *testing.T) {
|
||||||
|
now := time.Date(2026, 3, 28, 12, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
// Create enough issues to test pagination (PageSize = 20).
|
||||||
|
var issues []Issue
|
||||||
|
for i := 0; i < 25; i++ {
|
||||||
|
issues = append(issues, Issue{
|
||||||
|
ID: int64(i + 1),
|
||||||
|
Number: int64(i + 1),
|
||||||
|
Title: fmt.Sprintf("Issue %d", i+1),
|
||||||
|
State: "open",
|
||||||
|
UpdatedAt: now.Add(time.Duration(-i) * time.Hour),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch {
|
||||||
|
case r.URL.Path == "/api/v1/orgs/org1/repos":
|
||||||
|
json.NewEncoder(w).Encode([]Repo{
|
||||||
|
{ID: 1, Name: "repo-a", FullName: "org1/repo-a", Owner: struct {
|
||||||
|
Login string `json:"login"`
|
||||||
|
}{Login: "org1"}},
|
||||||
|
})
|
||||||
|
case r.URL.Path == "/api/v1/repos/org1/repo-a/issues":
|
||||||
|
json.NewEncoder(w).Encode(issues)
|
||||||
|
default:
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
c := NewClient(server.URL)
|
||||||
|
c.setCache("repos-org1", []Repo{
|
||||||
|
{ID: 1, Name: "repo-a", FullName: "org1/repo-a", Owner: struct {
|
||||||
|
Login string `json:"login"`
|
||||||
|
}{Login: "org1"}},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Page 1: should have 20 items with HasMore=true.
|
||||||
|
page1, err := c.ListAllIssues(context.Background(), "test-token", []string{"org1"}, "open", 1, "", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("page 1: %v", err)
|
||||||
|
}
|
||||||
|
if len(page1.Issues) != 20 {
|
||||||
|
t.Errorf("page 1: got %d issues, want 20", len(page1.Issues))
|
||||||
|
}
|
||||||
|
if !page1.HasMore {
|
||||||
|
t.Error("page 1: HasMore should be true")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Page 2: should have 5 items with HasMore=false.
|
||||||
|
page2, err := c.ListAllIssues(context.Background(), "test-token", []string{"org1"}, "open", 2, "", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("page 2: %v", err)
|
||||||
|
}
|
||||||
|
if len(page2.Issues) != 5 {
|
||||||
|
t.Errorf("page 2: got %d issues, want 5", len(page2.Issues))
|
||||||
|
}
|
||||||
|
if page2.HasMore {
|
||||||
|
t.Error("page 2: HasMore should be false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListAllPullRequests_Sorting(t *testing.T) {
|
||||||
|
server := newFanOutServer(t)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
c := NewClient(server.URL)
|
||||||
|
c.setCache("repos-org1", []Repo{
|
||||||
|
{ID: 1, Name: "repo-a", FullName: "org1/repo-a", Owner: struct {
|
||||||
|
Login string `json:"login"`
|
||||||
|
}{Login: "org1"}},
|
||||||
|
{ID: 2, Name: "repo-b", FullName: "org1/repo-b", Owner: struct {
|
||||||
|
Login string `json:"login"`
|
||||||
|
}{Login: "org1"}},
|
||||||
|
})
|
||||||
|
|
||||||
|
result, err := c.ListAllPullRequests(context.Background(), "test-token", []string{"org1"}, "open", 1, "", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result.Pulls) != 3 {
|
||||||
|
t.Fatalf("got %d PRs, want 3", len(result.Pulls))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should be sorted by UpdatedAt descending.
|
||||||
|
// PR B1 (-10m), PR A1 (-30m), PR B2 (-1h).
|
||||||
|
expected := []string{"PR B1", "PR A1", "PR B2"}
|
||||||
|
for i, title := range expected {
|
||||||
|
if result.Pulls[i].Title != title {
|
||||||
|
t.Errorf("pulls[%d].Title = %q, want %q", i, result.Pulls[i].Title, title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListAllPullRequests_StateFilter(t *testing.T) {
|
||||||
|
stateReceived := ""
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch {
|
||||||
|
case r.URL.Path == "/api/v1/orgs/org1/repos":
|
||||||
|
json.NewEncoder(w).Encode([]Repo{
|
||||||
|
{ID: 1, Name: "repo-a", FullName: "org1/repo-a", Owner: struct {
|
||||||
|
Login string `json:"login"`
|
||||||
|
}{Login: "org1"}},
|
||||||
|
})
|
||||||
|
case r.URL.Path == "/api/v1/repos/org1/repo-a/pulls":
|
||||||
|
stateReceived = r.URL.Query().Get("state")
|
||||||
|
json.NewEncoder(w).Encode([]PullRequest{})
|
||||||
|
default:
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
c := NewClient(server.URL)
|
||||||
|
c.setCache("repos-org1", []Repo{
|
||||||
|
{ID: 1, Name: "repo-a", FullName: "org1/repo-a", Owner: struct {
|
||||||
|
Login string `json:"login"`
|
||||||
|
}{Login: "org1"}},
|
||||||
|
})
|
||||||
|
|
||||||
|
_, err := c.ListAllPullRequests(context.Background(), "test-token", []string{"org1"}, "closed", 1, "", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if stateReceived != "closed" {
|
||||||
|
t.Errorf("state query param = %q, want %q", stateReceived, "closed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListAllPullRequests_Pagination(t *testing.T) {
|
||||||
|
now := time.Date(2026, 3, 28, 12, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
var prs []PullRequest
|
||||||
|
for i := 0; i < 25; i++ {
|
||||||
|
prs = append(prs, PullRequest{
|
||||||
|
ID: int64(i + 1),
|
||||||
|
Number: int64(i + 1),
|
||||||
|
Title: fmt.Sprintf("PR %d", i+1),
|
||||||
|
State: "open",
|
||||||
|
UpdatedAt: now.Add(time.Duration(-i) * time.Hour),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch {
|
||||||
|
case r.URL.Path == "/api/v1/orgs/org1/repos":
|
||||||
|
json.NewEncoder(w).Encode([]Repo{
|
||||||
|
{ID: 1, Name: "repo-a", FullName: "org1/repo-a", Owner: struct {
|
||||||
|
Login string `json:"login"`
|
||||||
|
}{Login: "org1"}},
|
||||||
|
})
|
||||||
|
case r.URL.Path == "/api/v1/repos/org1/repo-a/pulls":
|
||||||
|
json.NewEncoder(w).Encode(prs)
|
||||||
|
default:
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
c := NewClient(server.URL)
|
||||||
|
c.setCache("repos-org1", []Repo{
|
||||||
|
{ID: 1, Name: "repo-a", FullName: "org1/repo-a", Owner: struct {
|
||||||
|
Login string `json:"login"`
|
||||||
|
}{Login: "org1"}},
|
||||||
|
})
|
||||||
|
|
||||||
|
page1, err := c.ListAllPullRequests(context.Background(), "test-token", []string{"org1"}, "open", 1, "", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("page 1: %v", err)
|
||||||
|
}
|
||||||
|
if len(page1.Pulls) != 20 {
|
||||||
|
t.Errorf("page 1: got %d PRs, want 20", len(page1.Pulls))
|
||||||
|
}
|
||||||
|
if !page1.HasMore {
|
||||||
|
t.Error("page 1: HasMore should be true")
|
||||||
|
}
|
||||||
|
|
||||||
|
page2, err := c.ListAllPullRequests(context.Background(), "test-token", []string{"org1"}, "open", 2, "", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("page 2: %v", err)
|
||||||
|
}
|
||||||
|
if len(page2.Pulls) != 5 {
|
||||||
|
t.Errorf("page 2: got %d PRs, want 5", len(page2.Pulls))
|
||||||
|
}
|
||||||
|
if page2.HasMore {
|
||||||
|
t.Error("page 2: HasMore should be false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user