2e2fcbabfc
PR Ready Gate / clear-labels (pull_request) Successful in 2s
CI / test (pull_request) Successful in 19s
CI / review (gpt-5, security, ., rodin/security-patterns, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 38s
CI / review (anthropic--claude-4.6-sonnet, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 40s
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 56s
- Reorder stdlib imports in review_test.go to alphabetical (goimports convention) - Restore explanatory comment for nil body in DeleteReview Addresses review comments #20533, #20534 on PR #119
678 lines
19 KiB
Go
678 lines
19 KiB
Go
package github
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.weiker.me/rodin/review-bot/vcs"
|
|
)
|
|
|
|
// --- PostReview tests ---
|
|
|
|
func TestPostReview_HappyPath(t *testing.T) {
|
|
c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "POST" {
|
|
t.Fatalf("expected POST, got %s", r.Method)
|
|
}
|
|
if r.URL.Path != "/repos/owner/repo/pulls/5/reviews" {
|
|
t.Fatalf("unexpected path: %s", r.URL.Path)
|
|
}
|
|
if r.Header.Get("Content-Type") != "application/json" {
|
|
t.Errorf("expected Content-Type application/json, got %q", r.Header.Get("Content-Type"))
|
|
}
|
|
|
|
// Verify request body
|
|
body, _ := io.ReadAll(r.Body)
|
|
var req postReviewRequest
|
|
if err := json.Unmarshal(body, &req); err != nil {
|
|
t.Fatalf("unmarshal request: %v", err)
|
|
}
|
|
if req.Event != "APPROVE" {
|
|
t.Errorf("expected event APPROVE, got %q", req.Event)
|
|
}
|
|
if req.Body != "LGTM" {
|
|
t.Errorf("expected body 'LGTM', got %q", req.Body)
|
|
}
|
|
if req.CommitID != "abc123" {
|
|
t.Errorf("expected commit_id 'abc123', got %q", req.CommitID)
|
|
}
|
|
if len(req.Comments) != 1 {
|
|
t.Fatalf("expected 1 comment, got %d", len(req.Comments))
|
|
}
|
|
if req.Comments[0].Path != "main.go" {
|
|
t.Errorf("expected comment path 'main.go', got %q", req.Comments[0].Path)
|
|
}
|
|
if req.Comments[0].Position != 4 {
|
|
t.Errorf("expected comment position 4, got %d", req.Comments[0].Position)
|
|
}
|
|
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"id": 100,
|
|
"body": "LGTM",
|
|
"state": "APPROVED",
|
|
"commit_id": "abc123",
|
|
"user": map[string]string{"login": "reviewer"},
|
|
})
|
|
})
|
|
|
|
review, err := c.PostReview(context.Background(), "owner", "repo", 5, vcs.ReviewRequest{
|
|
Body: "LGTM",
|
|
Event: vcs.ReviewEventApprove,
|
|
Comments: []vcs.ReviewComment{
|
|
{Path: "main.go", Position: 4, CommitID: "abc123", Body: "nit: rename"},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if review.ID != 100 {
|
|
t.Errorf("expected ID 100, got %d", review.ID)
|
|
}
|
|
if review.Body != "LGTM" {
|
|
t.Errorf("expected body 'LGTM', got %q", review.Body)
|
|
}
|
|
if review.State != "APPROVED" {
|
|
t.Errorf("expected state 'APPROVED', got %q", review.State)
|
|
}
|
|
if review.User.Login != "reviewer" {
|
|
t.Errorf("expected user 'reviewer', got %q", review.User.Login)
|
|
}
|
|
if review.CommitID != "abc123" {
|
|
t.Errorf("expected commit_id 'abc123', got %q", review.CommitID)
|
|
}
|
|
}
|
|
|
|
func TestPostReview_401(t *testing.T) {
|
|
c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(401)
|
|
w.Write([]byte(`{"message":"Bad credentials"}`))
|
|
})
|
|
|
|
_, err := c.PostReview(context.Background(), "owner", "repo", 5, vcs.ReviewRequest{
|
|
Body: "LGTM",
|
|
Event: vcs.ReviewEventApprove,
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error for 401")
|
|
}
|
|
if !IsUnauthorized(err) {
|
|
t.Errorf("expected IsUnauthorized=true, got error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPostReview_422(t *testing.T) {
|
|
c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(422)
|
|
w.Write([]byte(`{"message":"Unprocessable Entity"}`))
|
|
})
|
|
|
|
_, err := c.PostReview(context.Background(), "owner", "repo", 5, vcs.ReviewRequest{
|
|
Body: "LGTM",
|
|
Event: vcs.ReviewEventApprove,
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error for 422")
|
|
}
|
|
// 422 should surface as a wrapped APIError
|
|
var apiErr *APIError
|
|
if !errors.As(err, &apiErr) {
|
|
t.Fatalf("expected *APIError, got %T: %v", err, err)
|
|
}
|
|
if apiErr.StatusCode != 422 {
|
|
t.Errorf("expected status 422, got %d", apiErr.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestPostReview_MalformedResponse(t *testing.T) {
|
|
c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
w.Write([]byte(`not json`))
|
|
})
|
|
|
|
_, err := c.PostReview(context.Background(), "owner", "repo", 5, vcs.ReviewRequest{
|
|
Body: "LGTM",
|
|
Event: vcs.ReviewEventApprove,
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error for malformed response")
|
|
}
|
|
if !strings.Contains(err.Error(), "parse review response") {
|
|
t.Errorf("expected parse error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
// --- ListReviews tests ---
|
|
|
|
func TestListReviews_HappyPath(t *testing.T) {
|
|
c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "GET" {
|
|
t.Fatalf("expected GET, got %s", r.Method)
|
|
}
|
|
if r.URL.Path != "/repos/owner/repo/pulls/3/reviews" {
|
|
t.Fatalf("unexpected path: %s", r.URL.Path)
|
|
}
|
|
json.NewEncoder(w).Encode([]map[string]interface{}{
|
|
{
|
|
"id": 1,
|
|
"body": "Approved",
|
|
"state": "APPROVED",
|
|
"commit_id": "sha1",
|
|
"user": map[string]string{"login": "user1"},
|
|
},
|
|
{
|
|
"id": 2,
|
|
"body": "Needs work",
|
|
"state": "CHANGES_REQUESTED",
|
|
"commit_id": "sha2",
|
|
"user": map[string]string{"login": "user2"},
|
|
},
|
|
{
|
|
"id": 3,
|
|
"body": "Comment only",
|
|
"state": "COMMENTED",
|
|
"commit_id": "sha3",
|
|
"user": map[string]string{"login": "user3"},
|
|
},
|
|
{
|
|
"id": 4,
|
|
"body": "Old review",
|
|
"state": "DISMISSED",
|
|
"commit_id": "sha4",
|
|
"user": map[string]string{"login": "user4"},
|
|
},
|
|
})
|
|
})
|
|
|
|
reviews, err := c.ListReviews(context.Background(), "owner", "repo", 3)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(reviews) != 4 {
|
|
t.Fatalf("expected 4 reviews, got %d", len(reviews))
|
|
}
|
|
|
|
// Check state translation
|
|
expected := []struct {
|
|
id int64
|
|
state string
|
|
}{
|
|
{1, "APPROVED"},
|
|
{2, "REQUEST_CHANGES"},
|
|
{3, "COMMENT"},
|
|
{4, "DISMISSED"},
|
|
}
|
|
for i, e := range expected {
|
|
if reviews[i].ID != e.id {
|
|
t.Errorf("review[%d]: expected ID %d, got %d", i, e.id, reviews[i].ID)
|
|
}
|
|
if reviews[i].State != e.state {
|
|
t.Errorf("review[%d]: expected state %q, got %q", i, e.state, reviews[i].State)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestListReviews_404(t *testing.T) {
|
|
c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(404)
|
|
w.Write([]byte(`{"message":"Not Found"}`))
|
|
})
|
|
|
|
_, err := c.ListReviews(context.Background(), "owner", "repo", 999)
|
|
if err == nil {
|
|
t.Fatal("expected error for 404")
|
|
}
|
|
if !IsNotFound(err) {
|
|
t.Errorf("expected IsNotFound=true, got error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestListReviews_401(t *testing.T) {
|
|
c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(401)
|
|
w.Write([]byte(`{"message":"Bad credentials"}`))
|
|
})
|
|
|
|
_, err := c.ListReviews(context.Background(), "owner", "repo", 3)
|
|
if err == nil {
|
|
t.Fatal("expected error for 401")
|
|
}
|
|
if !IsUnauthorized(err) {
|
|
t.Errorf("expected IsUnauthorized=true, got error: %v", err)
|
|
}
|
|
}
|
|
|
|
// --- DeleteReview tests ---
|
|
|
|
func TestDeleteReview_HappyPath(t *testing.T) {
|
|
c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "DELETE" {
|
|
t.Fatalf("expected DELETE, got %s", r.Method)
|
|
}
|
|
if r.URL.Path != "/repos/owner/repo/pulls/5/reviews/42" {
|
|
t.Fatalf("unexpected path: %s", r.URL.Path)
|
|
}
|
|
w.WriteHeader(204)
|
|
})
|
|
|
|
err := c.DeleteReview(context.Background(), "owner", "repo", 5, 42)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDeleteReview_422_SubmittedReview(t *testing.T) {
|
|
c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(422)
|
|
w.Write([]byte(`{"message":"Can not delete a non pending review"}`))
|
|
})
|
|
|
|
err := c.DeleteReview(context.Background(), "owner", "repo", 5, 42)
|
|
if err == nil {
|
|
t.Fatal("expected error for 422")
|
|
}
|
|
if !errors.Is(err, ErrCannotDeleteSubmittedReview) {
|
|
t.Errorf("expected ErrCannotDeleteSubmittedReview, got: %v", err)
|
|
}
|
|
}
|
|
|
|
// --- DismissReview tests ---
|
|
|
|
func TestDismissReview_HappyPath(t *testing.T) {
|
|
c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "PUT" {
|
|
t.Fatalf("expected PUT, got %s", r.Method)
|
|
}
|
|
if r.URL.Path != "/repos/owner/repo/pulls/5/reviews/10/dismissals" {
|
|
t.Fatalf("unexpected path: %s", r.URL.Path)
|
|
}
|
|
|
|
body, _ := io.ReadAll(r.Body)
|
|
var req dismissReviewRequest
|
|
if err := json.Unmarshal(body, &req); err != nil {
|
|
t.Fatalf("unmarshal request: %v", err)
|
|
}
|
|
if req.Message != "Superseded by new review" {
|
|
t.Errorf("expected message 'Superseded by new review', got %q", req.Message)
|
|
}
|
|
if req.Event != "DISMISS" {
|
|
t.Errorf("expected event 'DISMISS', got %q", req.Event)
|
|
}
|
|
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"id": 10,
|
|
"state": "DISMISSED",
|
|
})
|
|
})
|
|
|
|
err := c.DismissReview(context.Background(), "owner", "repo", 5, 10, "Superseded by new review")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDismissReview_404(t *testing.T) {
|
|
c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(404)
|
|
w.Write([]byte(`{"message":"Not Found"}`))
|
|
})
|
|
|
|
err := c.DismissReview(context.Background(), "owner", "repo", 5, 999, "dismiss")
|
|
if err == nil {
|
|
t.Fatal("expected error for 404")
|
|
}
|
|
if !IsNotFound(err) {
|
|
t.Errorf("expected IsNotFound=true, got error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDismissReview_401(t *testing.T) {
|
|
c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(401)
|
|
w.Write([]byte(`{"message":"Bad credentials"}`))
|
|
})
|
|
|
|
err := c.DismissReview(context.Background(), "owner", "repo", 5, 10, "dismiss")
|
|
if err == nil {
|
|
t.Fatal("expected error for 401")
|
|
}
|
|
if !IsUnauthorized(err) {
|
|
t.Errorf("expected IsUnauthorized=true, got error: %v", err)
|
|
}
|
|
}
|
|
|
|
// --- State translation tests ---
|
|
|
|
func TestTranslateGitHubReviewState(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
want string
|
|
}{
|
|
{"approved passes through", "APPROVED", "APPROVED"},
|
|
{"changes_requested maps to REQUEST_CHANGES", "CHANGES_REQUESTED", "REQUEST_CHANGES"},
|
|
{"commented maps to COMMENT", "COMMENTED", "COMMENT"},
|
|
{"dismissed passes through", "DISMISSED", "DISMISSED"},
|
|
{"unknown state passes through", "UNKNOWN_STATE", "UNKNOWN_STATE"},
|
|
{"empty string passes through", "", ""},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := translateGitHubReviewState(tt.input)
|
|
if got != tt.want {
|
|
t.Errorf("translateGitHubReviewState(%q) = %q, want %q", tt.input, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPostReview_ConflictingCommitIDs(t *testing.T) {
|
|
c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
t.Fatal("request should not be sent when commit IDs conflict")
|
|
})
|
|
|
|
_, err := c.PostReview(context.Background(), "owner", "repo", 5, vcs.ReviewRequest{
|
|
Body: "Review",
|
|
Event: vcs.ReviewEventComment,
|
|
Comments: []vcs.ReviewComment{
|
|
{Path: "a.go", Position: 1, CommitID: "sha-1", Body: "first"},
|
|
{Path: "b.go", Position: 2, CommitID: "sha-2", Body: "second"},
|
|
},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error for conflicting commit IDs")
|
|
}
|
|
if !errors.Is(err, ErrConflictingCommitIDs) {
|
|
t.Errorf("expected ErrConflictingCommitIDs, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPostReview_RequestCommitID_TakesPriority(t *testing.T) {
|
|
var gotPayload struct {
|
|
CommitID string `json:"commit_id"`
|
|
Body string `json:"body"`
|
|
}
|
|
|
|
c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
json.NewDecoder(r.Body).Decode(&gotPayload)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]any{
|
|
"id": 42,
|
|
"body": "LGTM",
|
|
"state": "APPROVED",
|
|
"commit_id": "req-level-sha",
|
|
"user": map[string]any{"login": "bot"},
|
|
})
|
|
})
|
|
|
|
review, err := c.PostReview(context.Background(), "owner", "repo", 1, vcs.ReviewRequest{
|
|
Body: "LGTM",
|
|
Event: vcs.ReviewEventApprove,
|
|
CommitID: "req-level-sha",
|
|
Comments: []vcs.ReviewComment{
|
|
{Path: "a.go", Position: 1, CommitID: "req-level-sha", Body: "looks good"},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if gotPayload.CommitID != "req-level-sha" {
|
|
t.Errorf("sent commit_id = %q, want %q", gotPayload.CommitID, "req-level-sha")
|
|
}
|
|
if review.CommitID != "req-level-sha" {
|
|
t.Errorf("review.CommitID = %q, want %q", review.CommitID, "req-level-sha")
|
|
}
|
|
}
|
|
|
|
func TestPostReview_RequestCommitID_ConflictsWithComment(t *testing.T) {
|
|
c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
t.Fatal("request should not be sent when commit IDs conflict")
|
|
})
|
|
|
|
// req.CommitID is set, and a comment has a different CommitID → conflict
|
|
_, err := c.PostReview(context.Background(), "owner", "repo", 1, vcs.ReviewRequest{
|
|
Body: "Review",
|
|
Event: vcs.ReviewEventComment,
|
|
CommitID: "req-sha",
|
|
Comments: []vcs.ReviewComment{
|
|
{Path: "a.go", Position: 1, CommitID: "different-sha", Body: "nit"},
|
|
},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error for conflicting commit IDs")
|
|
}
|
|
if !errors.Is(err, ErrConflictingCommitIDs) {
|
|
t.Errorf("expected ErrConflictingCommitIDs, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPostReview_RequestCommitID_FallbackToComment(t *testing.T) {
|
|
var gotPayload struct {
|
|
CommitID string `json:"commit_id"`
|
|
}
|
|
|
|
c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
json.NewDecoder(r.Body).Decode(&gotPayload)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]any{
|
|
"id": 43,
|
|
"body": "ok",
|
|
"state": "COMMENTED",
|
|
"commit_id": "comment-sha",
|
|
"user": map[string]any{"login": "bot"},
|
|
})
|
|
})
|
|
|
|
// req.CommitID is empty, so it falls back to the comment's CommitID
|
|
_, err := c.PostReview(context.Background(), "owner", "repo", 1, vcs.ReviewRequest{
|
|
Body: "ok",
|
|
Event: vcs.ReviewEventComment,
|
|
// CommitID intentionally empty
|
|
Comments: []vcs.ReviewComment{
|
|
{Path: "a.go", Position: 1, CommitID: "comment-sha", Body: "note"},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if gotPayload.CommitID != "comment-sha" {
|
|
t.Errorf("sent commit_id = %q, want %q (fallback from comment)", gotPayload.CommitID, "comment-sha")
|
|
}
|
|
}
|
|
|
|
// --- ListReviews pagination tests ---
|
|
|
|
func TestListReviews_MultiPage(t *testing.T) {
|
|
// Test multi-page pagination: 2 full pages + 1 partial page.
|
|
// pageSize=3, so pages return [3, 3, 2] reviews = 8 total.
|
|
const pageSize = 3
|
|
callCount := 0
|
|
|
|
c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "GET" {
|
|
t.Fatalf("expected GET, got %s", r.Method)
|
|
}
|
|
callCount++
|
|
|
|
page := r.URL.Query().Get("page")
|
|
var reviews []map[string]interface{}
|
|
|
|
switch page {
|
|
case "1":
|
|
for i := 1; i <= pageSize; i++ {
|
|
reviews = append(reviews, map[string]interface{}{
|
|
"id": i, "body": fmt.Sprintf("review %d", i),
|
|
"state": "APPROVED", "commit_id": "sha1",
|
|
"user": map[string]string{"login": "user1"},
|
|
})
|
|
}
|
|
case "2":
|
|
for i := pageSize + 1; i <= pageSize*2; i++ {
|
|
reviews = append(reviews, map[string]interface{}{
|
|
"id": i, "body": fmt.Sprintf("review %d", i),
|
|
"state": "COMMENTED", "commit_id": "sha1",
|
|
"user": map[string]string{"login": "user2"},
|
|
})
|
|
}
|
|
case "3":
|
|
// Partial page: only 2 reviews (less than pageSize)
|
|
for i := pageSize*2 + 1; i <= pageSize*2+2; i++ {
|
|
reviews = append(reviews, map[string]interface{}{
|
|
"id": i, "body": fmt.Sprintf("review %d", i),
|
|
"state": "CHANGES_REQUESTED", "commit_id": "sha1",
|
|
"user": map[string]string{"login": "user3"},
|
|
})
|
|
}
|
|
default:
|
|
t.Fatalf("unexpected page: %s", page)
|
|
}
|
|
|
|
json.NewEncoder(w).Encode(reviews)
|
|
})
|
|
c.SetReviewPagination(pageSize, 10)
|
|
|
|
reviews, err := c.ListReviews(context.Background(), "owner", "repo", 1)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(reviews) != 8 {
|
|
t.Fatalf("expected 8 reviews, got %d", len(reviews))
|
|
}
|
|
if callCount != 3 {
|
|
t.Errorf("expected 3 API calls, got %d", callCount)
|
|
}
|
|
|
|
// Verify reviews are correctly concatenated in order
|
|
for i, r := range reviews {
|
|
expectedID := int64(i + 1)
|
|
if r.ID != expectedID {
|
|
t.Errorf("review[%d]: expected ID %d, got %d", i, expectedID, r.ID)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestListReviews_ExactMultipleOfPageSize(t *testing.T) {
|
|
// When total reviews is an exact multiple of pageSize, an extra request
|
|
// returning 0 results terminates the loop. No truncation warning.
|
|
const pageSize = 2
|
|
callCount := 0
|
|
|
|
c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
callCount++
|
|
page := r.URL.Query().Get("page")
|
|
|
|
var reviews []map[string]interface{}
|
|
switch page {
|
|
case "1":
|
|
reviews = []map[string]interface{}{
|
|
{"id": 1, "body": "r1", "state": "APPROVED", "commit_id": "s1", "user": map[string]string{"login": "u1"}},
|
|
{"id": 2, "body": "r2", "state": "APPROVED", "commit_id": "s1", "user": map[string]string{"login": "u2"}},
|
|
}
|
|
case "2":
|
|
reviews = []map[string]interface{}{
|
|
{"id": 3, "body": "r3", "state": "APPROVED", "commit_id": "s1", "user": map[string]string{"login": "u3"}},
|
|
{"id": 4, "body": "r4", "state": "APPROVED", "commit_id": "s1", "user": map[string]string{"login": "u4"}},
|
|
}
|
|
case "3":
|
|
// Empty page — signals end of data
|
|
reviews = []map[string]interface{}{}
|
|
default:
|
|
t.Fatalf("unexpected page: %s", page)
|
|
}
|
|
|
|
json.NewEncoder(w).Encode(reviews)
|
|
})
|
|
c.SetReviewPagination(pageSize, 10)
|
|
|
|
reviews, err := c.ListReviews(context.Background(), "owner", "repo", 1)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(reviews) != 4 {
|
|
t.Fatalf("expected 4 reviews, got %d", len(reviews))
|
|
}
|
|
// 3 calls: page 1 (full), page 2 (full), page 3 (empty)
|
|
if callCount != 3 {
|
|
t.Errorf("expected 3 API calls, got %d", callCount)
|
|
}
|
|
}
|
|
|
|
func TestListReviews_MaxPagesCutoff(t *testing.T) {
|
|
// When maxPages is hit and the last page is full, results are truncated
|
|
// and a warning would fire (we verify the reviews are still returned).
|
|
const pageSize = 2
|
|
const maxPages = 2
|
|
callCount := 0
|
|
|
|
c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
callCount++
|
|
page := r.URL.Query().Get("page")
|
|
|
|
// Always return a full page (simulating more data exists)
|
|
var reviews []map[string]interface{}
|
|
var baseID int
|
|
switch page {
|
|
case "1":
|
|
baseID = 0
|
|
case "2":
|
|
baseID = pageSize
|
|
default:
|
|
t.Fatalf("unexpected page %s (should not exceed maxPages)", page)
|
|
}
|
|
for i := 1; i <= pageSize; i++ {
|
|
reviews = append(reviews, map[string]interface{}{
|
|
"id": baseID + i, "body": fmt.Sprintf("r%d", baseID+i),
|
|
"state": "APPROVED", "commit_id": "sha1",
|
|
"user": map[string]string{"login": "user"},
|
|
})
|
|
}
|
|
|
|
json.NewEncoder(w).Encode(reviews)
|
|
})
|
|
c.SetReviewPagination(pageSize, maxPages)
|
|
|
|
reviews, err := c.ListReviews(context.Background(), "owner", "repo", 1)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
// Should return all reviews fetched within the cap
|
|
expectedCount := pageSize * maxPages
|
|
if len(reviews) != expectedCount {
|
|
t.Fatalf("expected %d reviews, got %d", expectedCount, len(reviews))
|
|
}
|
|
if callCount != maxPages {
|
|
t.Errorf("expected %d API calls, got %d", maxPages, callCount)
|
|
}
|
|
// Verify concatenation order
|
|
for i, r := range reviews {
|
|
if r.ID != int64(i+1) {
|
|
t.Errorf("review[%d]: expected ID %d, got %d", i, i+1, r.ID)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestListReviews_EmptyFirstPage(t *testing.T) {
|
|
// PR with no reviews: first page returns empty array.
|
|
callCount := 0
|
|
c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
callCount++
|
|
json.NewEncoder(w).Encode([]map[string]interface{}{})
|
|
})
|
|
c.SetReviewPagination(10, 5)
|
|
|
|
reviews, err := c.ListReviews(context.Background(), "owner", "repo", 1)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(reviews) != 0 {
|
|
t.Fatalf("expected 0 reviews, got %d", len(reviews))
|
|
}
|
|
if callCount != 1 {
|
|
t.Errorf("expected 1 API call, got %d", callCount)
|
|
}
|
|
}
|