8b256360bf
Address review feedback from round-3 sonnet review: - PostReview doc comment now accurately describes vcs.ReviewEvent → GitHub wire-format string cast and notes nil-Comments omitempty behavior. - Rename 'expected' field to 'want' in TestTranslateGitHubReviewState to match the project's established naming convention.
382 lines
10 KiB
Go
382 lines
10 KiB
Go
package github
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.weiker.me/rodin/review-bot/vcs"
|
|
)
|
|
|
|
func newTestClient(t *testing.T, handler http.HandlerFunc) *Client {
|
|
t.Helper()
|
|
srv := httptest.NewServer(handler)
|
|
t.Cleanup(srv.Close)
|
|
c := NewClient("test-token", srv.URL, AllowInsecureHTTP())
|
|
c.SetHTTPClient(srv.Client())
|
|
if err := c.SetRetryBackoff([]time.Duration{1 * time.Millisecond, 1 * time.Millisecond}); err != nil {
|
|
t.Fatalf("SetRetryBackoff: %v", err)
|
|
}
|
|
return c
|
|
}
|
|
|
|
// --- PostReview tests ---
|
|
|
|
func TestPostReview_HappyPath(t *testing.T) {
|
|
c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "POST" {
|
|
t.Errorf("expected POST, got %s", r.Method)
|
|
}
|
|
if r.URL.Path != "/repos/owner/repo/pulls/5/reviews" {
|
|
t.Errorf("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.Errorf("expected GET, got %s", r.Method)
|
|
}
|
|
if r.URL.Path != "/repos/owner/repo/pulls/3/reviews" {
|
|
t.Errorf("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.Errorf("expected DELETE, got %s", r.Method)
|
|
}
|
|
if r.URL.Path != "/repos/owner/repo/pulls/5/reviews/42" {
|
|
t.Errorf("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.Errorf("expected PUT, got %s", r.Method)
|
|
}
|
|
if r.URL.Path != "/repos/owner/repo/pulls/5/reviews/10/dismissals" {
|
|
t.Errorf("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 {
|
|
input string
|
|
want string
|
|
}{
|
|
{"APPROVED", "APPROVED"},
|
|
{"CHANGES_REQUESTED", "REQUEST_CHANGES"},
|
|
{"COMMENTED", "COMMENT"},
|
|
{"DISMISSED", "DISMISSED"},
|
|
{"UNKNOWN_STATE", "UNKNOWN_STATE"},
|
|
{"", ""},
|
|
}
|
|
for _, tt := range tests {
|
|
got := translateGitHubReviewState(tt.input)
|
|
if got != tt.want {
|
|
t.Errorf("translateGitHubReviewState(%q) = %q, want %q", tt.input, got, tt.want)
|
|
}
|
|
}
|
|
}
|