Files
gitea-tea/internal/services/reviews_test.go

271 lines
12 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package services
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"testing"
gitea "gitea.dev/sdk"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestReviewCommentsContinueWhenServerCapsPageSize(t *testing.T) {
reviewPages, collections := 0, 0
client := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api/v1/repos/owner/repo/pulls/3/reviews" {
reviewPages++
switch r.URL.Query().Get("page") {
case "1":
_, _ = w.Write([]byte(`[{"id":1,"comments_count":1}]`))
case "2":
_, _ = w.Write([]byte(`[{"id":2,"comments_count":1}]`))
case "3":
_, _ = w.Write([]byte(`[]`))
default:
t.Errorf("unexpected review page %q", r.URL.Query().Get("page"))
w.WriteHeader(http.StatusBadRequest)
}
return
}
collections++
assert.Empty(t, r.URL.RawQuery, "per-review comments are unpaginated")
if r.URL.Path == "/api/v1/repos/owner/repo/pulls/3/reviews/1/comments" {
_, _ = w.Write([]byte(`[{"id":5,"position":4,"pull_request_review_id":1,"user":{"login":"alice"},"resolver":{"login":"bob"}}]`))
} else {
assert.Equal(t, "/api/v1/repos/owner/repo/pulls/3/reviews/2/comments", r.URL.Path)
_, _ = w.Write([]byte(`[{"id":2}]`))
}
}))
items, err := (Reviews{Client: client, Repository: "owner/repo"}).Comments(context.Background(), 3, 0, 1, 2)
require.NoError(t, err)
require.Len(t, items, 1)
require.EqualValues(t, 5, items[0].ID)
require.EqualValues(t, 4, items[0].Line)
require.Equal(t, "alice", items[0].Author)
require.Equal(t, "bob", items[0].Resolver)
require.Equal(t, 3, reviewPages)
require.Equal(t, 2, collections)
}
func TestInlineCommentsSkipPendingReviews(t *testing.T) {
var fetched []string
client := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fetched = append(fetched, r.URL.Path)
_, _ = w.Write([]byte(`[{"id":5,"pull_request_review_id":1}]`))
}))
reviews := []Review{{ID: 1, State: "COMMENT", CommentsCount: 1}, {ID: 2, State: "PENDING", CommentsCount: 1}}
items, err := (Reviews{Client: client, Repository: "owner/repo"}).Inline(context.Background(), 3, reviews)
require.NoError(t, err)
require.Len(t, items, 1)
require.Equal(t, []string{"/api/v1/repos/owner/repo/pulls/3/reviews/1/comments"}, fetched)
}
func TestReviewListPageReachesPastServerPageCap(t *testing.T) {
client := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
size, _ := strconv.Atoi(r.URL.Query().Get("limit"))
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
size = min(size, 2) // the server caps its pages at two items
var reviews []string
for id := (page-1)*size + 1; id <= min(page*size, 5); id++ {
reviews = append(reviews, fmt.Sprintf(`{"id":%d}`, id))
}
w.Header().Set("X-Total-Count", "5")
_, _ = w.Write([]byte("[" + strings.Join(reviews, ",") + "]"))
}))
window, err := (Reviews{Client: client, Repository: "owner/repo"}).ListPage(context.Background(), 3, 4, 1)
require.NoError(t, err)
require.Len(t, window.Items, 4)
require.EqualValues(t, 4, window.Items[3].ID)
require.True(t, window.More)
window, err = (Reviews{Client: client, Repository: "owner/repo"}).ListPage(context.Background(), 3, 4, 2)
require.NoError(t, err)
require.Len(t, window.Items, 1)
require.EqualValues(t, 5, window.Items[0].ID)
require.False(t, window.More)
}
func TestReviewDismissalReturnsTheUpdatedReview(t *testing.T) {
for _, dismiss := range []bool{false, true} {
t.Run(fmt.Sprintf("dismiss=%t", dismiss), func(t *testing.T) {
calls := 0
client := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
calls++
if r.Method == http.MethodGet {
assert.Equal(t, 2, calls, "the review is read after the mutation")
assert.Equal(t, "/api/v1/repos/owner/repo/pulls/1/reviews/2", r.URL.Path)
_ = json.NewEncoder(w).Encode(map[string]any{"id": 2, "dismissed": dismiss})
return
}
assert.Equal(t, http.MethodPost, r.Method)
if dismiss {
assert.Equal(t, "/api/v1/repos/owner/repo/pulls/1/reviews/2/dismissals", r.URL.Path)
var data map[string]string
assert.NoError(t, json.NewDecoder(r.Body).Decode(&data))
assert.Equal(t, map[string]string{"message": "Reason"}, data)
} else {
assert.Equal(t, "/api/v1/repos/owner/repo/pulls/1/reviews/2/undismissals", r.URL.Path)
assert.Zero(t, r.ContentLength)
}
_ = json.NewEncoder(w).Encode(map[string]any{"id": 2, "dismissed": dismiss})
}))
item, err := (Reviews{Client: client, Repository: "owner/repo"}).Dismiss(context.Background(), 1, 2, dismiss, "Reason")
require.NoError(t, err)
require.EqualValues(t, 2, item.ID)
require.Equal(t, dismiss, item.Dismissed)
require.Equal(t, 2, calls)
})
}
}
func TestReviewSubmitPreservesBodyUnlessReplaced(t *testing.T) {
empty := ""
for name, test := range map[string]struct {
body *string
wantBody string
wantGets int
}{
"omitted keeps current": {wantBody: "Pending notes", wantGets: 1},
"explicit empty clears": {body: &empty, wantBody: ""},
} {
t.Run(name, func(t *testing.T) {
gets := 0
var submitted map[string]string
client := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/api/v1/repos/owner/repo/pulls/1/reviews/2", r.URL.Path)
if r.Method == http.MethodGet {
gets++
_, _ = w.Write([]byte(`{"id":2,"body":"Pending notes","state":"PENDING"}`))
return
}
assert.Equal(t, http.MethodPost, r.Method)
assert.NoError(t, json.NewDecoder(r.Body).Decode(&submitted))
_, _ = w.Write([]byte(`{"id":2,"state":"APPROVED"}`))
}))
item, err := (Reviews{Client: client, Repository: "owner/repo"}).Submit(context.Background(), 1, 2, ReviewSubmitOptions{Event: "APPROVED", Body: test.body})
require.NoError(t, err)
require.EqualValues(t, 2, item.ID)
require.Equal(t, test.wantGets, gets)
require.Equal(t, map[string]string{"event": "APPROVED", "body": test.wantBody}, submitted)
})
}
}
func TestReviewSubmitAllowsEmptyCommentBody(t *testing.T) {
var submitted map[string]string
client := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "POST /api/v1/repos/owner/repo/pulls/1/reviews/2", r.Method+" "+r.URL.Path)
assert.NoError(t, json.NewDecoder(r.Body).Decode(&submitted))
_, _ = w.Write([]byte(`{"id":2,"state":"COMMENT","comments_count":1}`))
}))
empty := ""
item, err := (Reviews{Client: client, Repository: "owner/repo"}).Submit(context.Background(), 1, 2, ReviewSubmitOptions{Event: "COMMENT", Body: &empty})
require.NoError(t, err)
require.Equal(t, Review{ID: 2, State: "COMMENT", CommentsCount: 1}, item)
require.Equal(t, map[string]string{"event": "COMMENT", "body": ""}, submitted)
}
func TestReviewCreateSendsInlineComments(t *testing.T) {
var created struct {
Event string `json:"event"`
Body string `json:"body"`
Commit string `json:"commit_id"`
Comments []InlineComment
}
client := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodPost, r.Method)
assert.Equal(t, "/api/v1/repos/owner/repo/pulls/1/reviews", r.URL.Path)
assert.NoError(t, json.NewDecoder(r.Body).Decode(&created))
_, _ = w.Write([]byte(`{"id":7,"state":"REQUEST_CHANGES","user":{"login":"alice"},"commit_id":"abc"}`))
}))
item, err := (Reviews{Client: client, Repository: "owner/repo"}).Create(context.Background(), 1, ReviewOptions{
Event: "REQUEST_CHANGES", Body: "Fix", Commit: "abc",
Comments: []InlineComment{{Path: "a.go", Body: "here", NewLine: 3}, {Path: "b.go", Body: "there", OldLine: 2}},
})
require.NoError(t, err)
require.Equal(t, Review{ID: 7, State: "REQUEST_CHANGES", Author: "alice", Commit: "abc"}, item)
require.Equal(t, "REQUEST_CHANGES", created.Event)
require.Equal(t, "Fix", created.Body)
require.Equal(t, "abc", created.Commit)
require.Equal(t, []InlineComment{{Path: "a.go", Body: "here", NewLine: 3}, {Path: "b.go", Body: "there", OldLine: 2}}, created.Comments)
}
func TestReviewResolveAndReplyUseCommentEndpoints(t *testing.T) {
paths := []string{}
client := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodPost, r.Method)
paths = append(paths, r.URL.Path)
if r.URL.Path == "/api/v1/repos/owner/repo/pulls/1/comments/5/replies" {
var reply map[string]string
assert.NoError(t, json.NewDecoder(r.Body).Decode(&reply))
assert.Equal(t, map[string]string{"body": "Done"}, reply)
_, _ = w.Write([]byte(`{"id":6,"body":"Done","user":{"login":"bob"},"pull_request_review_id":3}`))
return
}
w.WriteHeader(http.StatusOK)
}))
service := Reviews{Client: client, Repository: "owner/repo"}
require.NoError(t, service.Resolve(context.Background(), 5, true))
require.NoError(t, service.Resolve(context.Background(), 5, false))
reply, err := service.Reply(context.Background(), 1, 5, "Done")
require.NoError(t, err)
require.Equal(t, ReviewComment{ID: 6, ReviewID: 3, Body: "Done", Author: "bob"}, reply)
require.Equal(t, []string{
"/api/v1/repos/owner/repo/pulls/comments/5/resolve",
"/api/v1/repos/owner/repo/pulls/comments/5/unresolve",
"/api/v1/repos/owner/repo/pulls/1/comments/5/replies",
}, paths)
}
func TestReviewDecisionIgnoresCommentReviews(t *testing.T) {
reviews := []Review{
{ID: 1, Author: "bob", State: "REQUEST_CHANGES"},
{ID: 2, Author: "bob", State: "COMMENT"},
{ID: 3, Author: "carol", State: "APPROVED"},
}
assert.Equal(t, "changes_requested", ReviewDecision(reviews, false))
assert.Equal(t, "approved", ReviewDecision([]Review{{ID: 1, Author: "carol", State: "APPROVED"}, {ID: 2, Author: "carol", State: "COMMENT"}}, false))
assert.Equal(t, "", ReviewDecision([]Review{{ID: 1, Author: "carol", State: "COMMENT"}}, false))
}
func TestReviewFromMapsServerFields(t *testing.T) {
review := decode[gitea.PullReview](t, `{"id":2,"body":"LGTM","state":"APPROVED","user":{"login":"alice"},"html_url":"https://gitea.test/r/2",
"commit_id":"abc","submitted_at":"2026-10-01T12:00:00Z","comments_count":3,"dismissed":true}`)
require.Equal(t, Review{
ID: 2, Body: "LGTM", State: "APPROVED", Author: "alice", URL: "https://gitea.test/r/2", Commit: "abc",
SubmittedAt: "2026-10-01T12:00:00Z", CommentsCount: 3, Dismissed: true,
}, reviewFrom(review))
}
func TestReviewFromNamesRequestedTeam(t *testing.T) {
review := reviewFrom(decode[gitea.PullReview](t, `{"id":251,"state":"REQUEST_REVIEW","user":null,"team":{"name":"devs"}}`))
require.Equal(t, "devs", review.Team)
require.Equal(t, "team devs", review.Reviewer())
require.Equal(t, "alice", Review{Author: "alice", Team: "devs"}.Reviewer())
}
func TestReviewCommentFromMapsServerFields(t *testing.T) {
comment := decode[gitea.PullReviewComment](t, `{"id":5,"pull_request_review_id":1,"body":"Nit","user":{"login":"alice"},
"resolver":{"login":"bob"},"html_url":"https://gitea.test/c/5","path":"main.go","position":12,
"created_at":"2026-10-01T12:00:00Z","updated_at":"2026-10-02T12:00:00Z"}`)
require.Equal(t, ReviewComment{
ID: 5, ReviewID: 1, Body: "Nit", Author: "alice", Resolver: "bob", URL: "https://gitea.test/c/5", Path: "main.go", Line: 12,
CreatedAt: "2026-10-01T12:00:00Z", UpdatedAt: "2026-10-02T12:00:00Z",
}, reviewCommentFrom(comment))
}
func TestReviewCommentLocationNamesRemovedLines(t *testing.T) {
removed := reviewCommentFrom(decode[gitea.PullReviewComment](t, `{"id":6,"path":"main.go","position":0,"original_position":4}`))
require.EqualValues(t, 4, removed.OldLine)
require.Equal(t, "main.go:4 (old)", removed.Location())
require.Equal(t, "main.go:12", ReviewComment{Path: "main.go", Line: 12}.Location())
require.Equal(t, "main.go", ReviewComment{Path: "main.go"}.Location())
}