Files
Gitea/tests/integration/api_pull_review_test.go
T
Giteabotandsilverwind 6f5d66f6dd fix(pull): keep review body in sync when its comment is edited (#40058) (#40133)
Backport #40058 by @silverwind

A review's text is stored on both the review and its timeline comment,
but editing it in the web UI or through the issue comment API only
updated the comment. The reviews API kept returning the old body
afterwards.

Extracted out of https://github.com/go-gitea/gitea/pull/39865.

*Written by Claude.*

Co-authored-by: silverwind <me@silverwind.io>
2026-10-31 18:21:03 +01:00

638 lines
30 KiB
Go

// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"fmt"
"net/http"
"testing"
auth_model "gitea.dev/models/auth"
"gitea.dev/models/db"
issues_model "gitea.dev/models/issues"
access_model "gitea.dev/models/perm/access"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unittest"
user_model "gitea.dev/models/user"
"gitea.dev/modules/git"
"gitea.dev/modules/json"
api "gitea.dev/modules/structs"
issue_service "gitea.dev/services/issue"
pull_service "gitea.dev/services/pull"
"gitea.dev/tests"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"xorm.io/builder"
)
func TestAPIPullReview(t *testing.T) {
defer tests.PrepareTestEnv(t)()
t.Run("General", testAPIPullReviewGeneral)
t.Run("CommentReply", testAPIPullReviewCommentReply)
}
func testAPIPullReviewGeneral(t *testing.T) {
pullIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3})
assert.NoError(t, pullIssue.LoadAttributes(t.Context()))
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pullIssue.RepoID})
// test ListPullReviews
session := loginUser(t, "user2")
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
req := NewRequestf(t, http.MethodGet, "/api/v1/repos/%s/%s/pulls/%d/reviews", repo.OwnerName, repo.Name, pullIssue.Index).
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
reviews := DecodeJSON(t, resp, []*api.PullReview{})
require.Len(t, reviews, 8)
for _, r := range reviews {
assert.Equal(t, pullIssue.HTMLURL(t.Context()), r.HTMLPullURL)
}
assert.EqualValues(t, 8, reviews[3].ID)
assert.EqualValues(t, "APPROVED", reviews[3].State)
assert.Equal(t, 0, reviews[3].CodeCommentsCount)
assert.True(t, reviews[3].Stale)
assert.False(t, reviews[3].Official)
assert.EqualValues(t, 10, reviews[5].ID)
assert.EqualValues(t, "REQUEST_CHANGES", reviews[5].State)
assert.Equal(t, 1, reviews[5].CodeCommentsCount)
assert.EqualValues(t, -1, reviews[5].Reviewer.ID) // ghost user
assert.False(t, reviews[5].Stale)
assert.True(t, reviews[5].Official)
// test GetPullReview
req = NewRequestf(t, http.MethodGet, "/api/v1/repos/%s/%s/pulls/%d/reviews/%d", repo.OwnerName, repo.Name, pullIssue.Index, reviews[3].ID).
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
review := DecodeJSON(t, resp, &api.PullReview{})
assert.Equal(t, reviews[3], review)
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/pulls/%d/reviews/%d", repo.OwnerName, repo.Name, pullIssue.Index, reviews[5].ID).
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
review = DecodeJSON(t, resp, &api.PullReview{})
assert.Equal(t, reviews[5], review)
// test GetPullReviewComments
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 7})
req = NewRequestf(t, http.MethodGet, "/api/v1/repos/%s/%s/pulls/%d/reviews/%d/comments", repo.OwnerName, repo.Name, pullIssue.Index, 10).
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
reviewComments := DecodeJSON(t, resp, []*api.PullReviewComment{})
assert.Len(t, reviewComments, 1)
assert.Equal(t, "Ghost", reviewComments[0].Poster.UserName)
assert.Equal(t, "a review from a deleted user", reviewComments[0].Body)
assert.Equal(t, comment.ID, reviewComments[0].ID)
assert.EqualValues(t, comment.UpdatedUnix, reviewComments[0].Updated.Unix())
assert.Equal(t, comment.HTMLURL(t.Context()), reviewComments[0].HTMLURL)
// test CreatePullReview
req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews", repo.OwnerName, repo.Name, pullIssue.Index), &api.CreatePullReviewOptions{
Body: "body1",
// Event: "" # will result in PENDING
Comments: []api.CreatePullReviewComment{
{
Path: "README.md",
Body: "first new line",
OldLineNum: 0,
NewLineNum: 1,
}, {
Path: "README.md",
Body: "first old line",
OldLineNum: 1,
NewLineNum: 0,
}, {
Path: "iso-8859-1.txt",
Body: "this line contains a non-utf-8 character",
OldLineNum: 0,
NewLineNum: 1,
},
},
}).AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
review = DecodeJSON(t, resp, &api.PullReview{})
assert.EqualValues(t, 6, review.ID)
assert.EqualValues(t, "PENDING", review.State)
assert.Equal(t, 3, review.CodeCommentsCount)
// test SubmitPullReview
req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews/%d", repo.OwnerName, repo.Name, pullIssue.Index, review.ID), &api.SubmitPullReviewOptions{
Event: "APPROVED",
Body: "just two nits",
}).AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
review = DecodeJSON(t, resp, &api.PullReview{})
assert.EqualValues(t, 6, review.ID)
assert.EqualValues(t, "APPROVED", review.State)
assert.Equal(t, 3, review.CodeCommentsCount)
reviewComment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ReviewID: review.ID, Type: issues_model.CommentTypeReview})
req = NewRequestWithJSON(t, http.MethodPatch, fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d", repo.OwnerName, repo.Name, reviewComment.ID), &api.EditIssueCommentOption{Body: "just one nit"}).AddTokenAuth(getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue))
MakeRequest(t, req, http.StatusOK)
req = NewRequest(t, http.MethodGet, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews/%d", repo.OwnerName, repo.Name, pullIssue.Index, review.ID)).AddTokenAuth(token)
assert.Equal(t, "just one nit", DecodeJSON(t, MakeRequest(t, req, http.StatusOK), &api.PullReview{}).Body)
// test dismiss review
req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews/%d/dismissals", repo.OwnerName, repo.Name, pullIssue.Index, review.ID), &api.DismissPullReviewOptions{
Message: "test",
}).AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
review = DecodeJSON(t, resp, &api.PullReview{})
assert.EqualValues(t, 6, review.ID)
assert.True(t, review.Dismissed)
// test dismiss review
req = NewRequest(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews/%d/undismissals", repo.OwnerName, repo.Name, pullIssue.Index, review.ID)).
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
review = DecodeJSON(t, resp, &api.PullReview{})
assert.EqualValues(t, 6, review.ID)
assert.False(t, review.Dismissed)
// test DeletePullReview
req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews", repo.OwnerName, repo.Name, pullIssue.Index), &api.CreatePullReviewOptions{
Body: "just a comment",
Event: "COMMENT",
}).AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
review = DecodeJSON(t, resp, &api.PullReview{})
assert.EqualValues(t, "COMMENT", review.State)
assert.Equal(t, 0, review.CodeCommentsCount)
req = NewRequestf(t, http.MethodDelete, "/api/v1/repos/%s/%s/pulls/%d/reviews/%d", repo.OwnerName, repo.Name, pullIssue.Index, review.ID).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
reviewsURL := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews", repo.OwnerName, repo.Name, pullIssue.Index)
pendingComment := []api.CreatePullReviewComment{{Path: "README.md", Body: "pending comment", NewLineNum: 1}}
req = NewRequestWithJSON(t, http.MethodPost, reviewsURL, &api.CreatePullReviewOptions{Body: "draft"}).AddTokenAuth(token)
pendingReviewURL := fmt.Sprintf("%s/%d", reviewsURL, DecodeJSON(t, MakeRequest(t, req, http.StatusOK), &api.PullReview{}).ID)
req = NewRequestWithJSON(t, http.MethodPost, pendingReviewURL, &api.SubmitPullReviewOptions{Event: "COMMENT"}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusUnprocessableEntity)
req = NewRequestWithJSON(t, http.MethodPost, reviewsURL, &api.CreatePullReviewOptions{Comments: pendingComment}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
req = NewRequestWithJSON(t, http.MethodPost, pendingReviewURL, &api.SubmitPullReviewOptions{Event: "COMMENT"}).AddTokenAuth(token)
review = DecodeJSON(t, MakeRequest(t, req, http.StatusOK), &api.PullReview{})
assert.EqualValues(t, "COMMENT", review.State)
assert.Equal(t, 1, review.CodeCommentsCount)
req = NewRequestWithJSON(t, http.MethodPost, reviewsURL, &api.CreatePullReviewOptions{Comments: pendingComment}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
req = NewRequestWithJSON(t, http.MethodPost, reviewsURL, &api.CreatePullReviewOptions{Event: "COMMENT"}).AddTokenAuth(token)
review = DecodeJSON(t, MakeRequest(t, req, http.StatusOK), &api.PullReview{})
assert.EqualValues(t, "COMMENT", review.State)
assert.Equal(t, 1, review.CodeCommentsCount)
// test CreatePullReview Comment without body but with comments
req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews", repo.OwnerName, repo.Name, pullIssue.Index), &api.CreatePullReviewOptions{
// Body: "",
Event: "COMMENT",
Comments: []api.CreatePullReviewComment{
{
Path: "README.md",
Body: "first new line",
OldLineNum: 0,
NewLineNum: 1,
}, {
Path: "README.md",
Body: "first old line",
OldLineNum: 1,
NewLineNum: 0,
},
},
}).AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
commentReview := DecodeJSON(t, resp, &api.PullReview{})
assert.EqualValues(t, "COMMENT", commentReview.State)
assert.Equal(t, 2, commentReview.CodeCommentsCount)
assert.Empty(t, commentReview.Body)
assert.False(t, commentReview.Dismissed)
// test CreatePullReview Comment with body but without comments
commentBody := "This is a body of the comment."
req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews", repo.OwnerName, repo.Name, pullIssue.Index), &api.CreatePullReviewOptions{
Body: commentBody,
Event: "COMMENT",
Comments: []api.CreatePullReviewComment{},
}).AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
commentReview = DecodeJSON(t, resp, &api.PullReview{})
assert.EqualValues(t, "COMMENT", commentReview.State)
assert.Equal(t, 0, commentReview.CodeCommentsCount)
assert.Equal(t, commentBody, commentReview.Body)
assert.False(t, commentReview.Dismissed)
// test CreatePullReview Comment without body and no comments
req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews", repo.OwnerName, repo.Name, pullIssue.Index), &api.CreatePullReviewOptions{
Body: "",
Event: "COMMENT",
Comments: []api.CreatePullReviewComment{},
}).AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusUnprocessableEntity)
errMap := make(map[string]any)
json.Unmarshal(resp.Body.Bytes(), &errMap)
assert.Equal(t, "review requires a body or a comment", errMap["message"])
// test get review requests
// to make it simple, use same api with get review
pullIssue12 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 12})
assert.NoError(t, pullIssue12.LoadAttributes(t.Context()))
repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pullIssue12.RepoID})
req = NewRequestf(t, http.MethodGet, "/api/v1/repos/%s/%s/pulls/%d/reviews", repo3.OwnerName, repo3.Name, pullIssue12.Index).
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
reviews = DecodeJSON(t, resp, []*api.PullReview{})
assert.EqualValues(t, 11, reviews[0].ID)
assert.EqualValues(t, "REQUEST_REVIEW", reviews[0].State)
assert.Equal(t, 0, reviews[0].CodeCommentsCount)
assert.False(t, reviews[0].Stale)
assert.True(t, reviews[0].Official)
assert.Equal(t, "test_team", reviews[0].ReviewerTeam.Name)
assert.EqualValues(t, 12, reviews[1].ID)
assert.EqualValues(t, "REQUEST_REVIEW", reviews[1].State)
assert.Equal(t, 0, reviews[0].CodeCommentsCount)
assert.False(t, reviews[1].Stale)
assert.True(t, reviews[1].Official)
assert.EqualValues(t, 1, reviews[1].Reviewer.ID)
}
func TestAPIPullReviewRequest(t *testing.T) {
defer tests.PrepareTestEnv(t)()
pullIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3})
assert.NoError(t, pullIssue.LoadAttributes(t.Context()))
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pullIssue.RepoID})
// Test add Review Request
session := loginUser(t, "user2")
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", repo.OwnerName, repo.Name, pullIssue.Index), &api.PullReviewRequestOptions{
Reviewers: []string{"user4@example.com", "user8"},
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusCreated)
// poster of pr can't be reviewer
req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", repo.OwnerName, repo.Name, pullIssue.Index), &api.PullReviewRequestOptions{
Reviewers: []string{"user1"},
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusUnprocessableEntity)
// test user not exist
req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", repo.OwnerName, repo.Name, pullIssue.Index), &api.PullReviewRequestOptions{
Reviewers: []string{"testOther"},
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
// Test Remove Review Request
session2 := loginUser(t, "user4")
token2 := getTokenForLoggedInUser(t, session2, auth_model.AccessTokenScopeWriteRepository)
req = NewRequestWithJSON(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", repo.OwnerName, repo.Name, pullIssue.Index), &api.PullReviewRequestOptions{
Reviewers: []string{"user4"},
}).AddTokenAuth(token2)
MakeRequest(t, req, http.StatusNoContent)
// doer is not admin
req = NewRequestWithJSON(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", repo.OwnerName, repo.Name, pullIssue.Index), &api.PullReviewRequestOptions{
Reviewers: []string{"user8"},
}).AddTokenAuth(token2)
MakeRequest(t, req, http.StatusUnprocessableEntity)
req = NewRequestWithJSON(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", repo.OwnerName, repo.Name, pullIssue.Index), &api.PullReviewRequestOptions{
Reviewers: []string{"user8"},
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
// a collaborator can add/remove a review request
pullIssue21 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 21})
assert.NoError(t, pullIssue21.LoadAttributes(t.Context()))
pull21Repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pullIssue21.RepoID}) // repo60
user38Session := loginUser(t, "user38")
user38Token := getTokenForLoggedInUser(t, user38Session, auth_model.AccessTokenScopeWriteRepository)
req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", pull21Repo.OwnerName, pull21Repo.Name, pullIssue21.Index), &api.PullReviewRequestOptions{
Reviewers: []string{"user4@example.com"},
}).AddTokenAuth(user38Token)
MakeRequest(t, req, http.StatusCreated)
req = NewRequestWithJSON(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", pull21Repo.OwnerName, pull21Repo.Name, pullIssue21.Index), &api.PullReviewRequestOptions{
Reviewers: []string{"user4@example.com"},
}).AddTokenAuth(user38Token)
MakeRequest(t, req, http.StatusNoContent)
// the poster of the PR can add/remove a review request
user39Session := loginUser(t, "user39")
user39Token := getTokenForLoggedInUser(t, user39Session, auth_model.AccessTokenScopeWriteRepository)
req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", pull21Repo.OwnerName, pull21Repo.Name, pullIssue21.Index), &api.PullReviewRequestOptions{
Reviewers: []string{"user8"},
}).AddTokenAuth(user39Token)
MakeRequest(t, req, http.StatusCreated)
req = NewRequestWithJSON(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", pull21Repo.OwnerName, pull21Repo.Name, pullIssue21.Index), &api.PullReviewRequestOptions{
Reviewers: []string{"user8"},
}).AddTokenAuth(user39Token)
MakeRequest(t, req, http.StatusNoContent)
// user with read permission on pull requests unit can add/remove a review request
pullIssue22 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 22})
assert.NoError(t, pullIssue22.LoadAttributes(t.Context()))
pull22Repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pullIssue22.RepoID}) // repo61
req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", pull22Repo.OwnerName, pull22Repo.Name, pullIssue22.Index), &api.PullReviewRequestOptions{
Reviewers: []string{"user38"},
}).AddTokenAuth(user39Token) // user39 is from a team with read permission on pull requests unit
MakeRequest(t, req, http.StatusCreated)
req = NewRequestWithJSON(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", pull22Repo.OwnerName, pull22Repo.Name, pullIssue22.Index), &api.PullReviewRequestOptions{
Reviewers: []string{"user38"},
}).AddTokenAuth(user39Token) // user39 is from a team with read permission on pull requests unit
MakeRequest(t, req, http.StatusNoContent)
// Test team review request
pullIssue12 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 12})
assert.NoError(t, pullIssue12.LoadAttributes(t.Context()))
repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pullIssue12.RepoID})
// Test add Team Review Request
req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", repo3.OwnerName, repo3.Name, pullIssue12.Index), &api.PullReviewRequestOptions{
TeamReviewers: []string{"team1", "owners"},
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusCreated)
// Test add Team Review Request to not allowned
req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", repo3.OwnerName, repo3.Name, pullIssue12.Index), &api.PullReviewRequestOptions{
TeamReviewers: []string{"test_team"},
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusUnprocessableEntity)
// Test add Team Review Request to not exist
req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", repo3.OwnerName, repo3.Name, pullIssue12.Index), &api.PullReviewRequestOptions{
TeamReviewers: []string{"not_exist_team"},
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
// Test Remove team Review Request
req = NewRequestWithJSON(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", repo3.OwnerName, repo3.Name, pullIssue12.Index), &api.PullReviewRequestOptions{
TeamReviewers: []string{"team1"},
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
// empty request test
req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", repo3.OwnerName, repo3.Name, pullIssue12.Index), &api.PullReviewRequestOptions{}).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusCreated)
req = NewRequestWithJSON(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", repo3.OwnerName, repo3.Name, pullIssue12.Index), &api.PullReviewRequestOptions{}).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
}
func TestAPIPullReviewCommentResolveEndpoints(t *testing.T) {
defer tests.PrepareTestEnv(t)()
ctx := t.Context()
pullIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3})
require.NoError(t, pullIssue.LoadAttributes(ctx))
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pullIssue.RepoID})
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: pullIssue.PosterID})
require.NoError(t, pullIssue.LoadPullRequest(ctx))
gitRepo, err := git.OpenRepository(ctx, repo)
require.NoError(t, err)
defer gitRepo.Close()
latestCommitID, err := gitRepo.GetRefCommitID(t.Context(), pullIssue.PullRequest.GetGitHeadRefName())
require.NoError(t, err)
codeComment, err := pull_service.CreateCodeComment(ctx, doer, gitRepo, pullIssue, 1, "resolve comment", "README.md", false, 0, latestCommitID, nil)
require.NoError(t, err)
require.NotNil(t, codeComment)
session := loginUser(t, doer.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
resolveURL := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/comments/%d/resolve", repo.OwnerName, repo.Name, codeComment.ID)
unresolveURL := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/comments/%d/unresolve", repo.OwnerName, repo.Name, codeComment.ID)
req := NewRequest(t, http.MethodPost, resolveURL).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
// Verify comment is resolved
updatedComment, err := issues_model.GetCommentByID(ctx, codeComment.ID)
require.NoError(t, err)
assert.NotZero(t, updatedComment.ResolveDoerID)
assert.Equal(t, doer.ID, updatedComment.ResolveDoerID)
// Resolving again should be idempotent
MakeRequest(t, req, http.StatusNoContent)
req = NewRequest(t, http.MethodPost, unresolveURL).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
// Verify comment is unresolved
updatedComment, err = issues_model.GetCommentByID(ctx, codeComment.ID)
require.NoError(t, err)
assert.Zero(t, updatedComment.ResolveDoerID)
// Unresolving again should be idempotent
MakeRequest(t, req, http.StatusNoContent)
// Non-existing comment ID
req = NewRequest(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/comments/999999/resolve", repo.OwnerName, repo.Name)).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
// Non-code-comment
plainComment, err := issue_service.CreateIssueComment(ctx, doer, repo, pullIssue, "not a review comment", nil)
require.NoError(t, err)
req = NewRequest(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/comments/%d/resolve", repo.OwnerName, repo.Name, plainComment.ID)).AddTokenAuth(token)
MakeRequest(t, req, http.StatusBadRequest)
// Test permission check: use a user without write access for target repo to test 403 response
unauthorizedUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
require.NotEqual(t, pullIssue.PosterID, unauthorizedUser.ID)
unauthorizedSession := loginUser(t, unauthorizedUser.Name)
unauthorizedToken := getTokenForLoggedInUser(t, unauthorizedSession, auth_model.AccessTokenScopeWriteIssue, auth_model.AccessTokenScopeWriteRepository)
req = NewRequest(t, http.MethodGet, fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d", repo.OwnerName, repo.Name, plainComment.ID)).AddTokenAuth(unauthorizedToken)
MakeRequest(t, req, http.StatusOK)
req = NewRequest(t, http.MethodPost, resolveURL).AddTokenAuth(unauthorizedToken)
MakeRequest(t, req, http.StatusForbidden)
}
func TestAPIPullReviewStayDismissed(t *testing.T) {
// This test against issue https://github.com/go-gitea/gitea/issues/28542
// where old reviews surface after a review request got dismissed.
defer tests.PrepareTestEnv(t)()
pullIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3})
assert.NoError(t, pullIssue.LoadAttributes(t.Context()))
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pullIssue.RepoID})
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
session2 := loginUser(t, user2.LoginName)
token2 := getTokenForLoggedInUser(t, session2, auth_model.AccessTokenScopeWriteRepository)
user8 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 8})
session8 := loginUser(t, user8.LoginName)
token8 := getTokenForLoggedInUser(t, session8, auth_model.AccessTokenScopeWriteRepository)
// user2 request user8
req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", repo.OwnerName, repo.Name, pullIssue.Index), &api.PullReviewRequestOptions{
Reviewers: []string{user8.LoginName},
}).AddTokenAuth(token2)
MakeRequest(t, req, http.StatusCreated)
reviewsCountCheck(t,
"check we have only one review request",
pullIssue.ID, user8.ID, 0, 1, 1, false)
// user2 request user8 again, it is expected to be ignored
req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", repo.OwnerName, repo.Name, pullIssue.Index), &api.PullReviewRequestOptions{
Reviewers: []string{user8.LoginName},
}).AddTokenAuth(token2)
MakeRequest(t, req, http.StatusCreated)
reviewsCountCheck(t,
"check we have only one review request, even after re-request it again",
pullIssue.ID, user8.ID, 0, 1, 1, false)
// user8 reviews it as accept
req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews", repo.OwnerName, repo.Name, pullIssue.Index), &api.CreatePullReviewOptions{
Event: "APPROVED",
Body: "lgtm",
}).AddTokenAuth(token8)
MakeRequest(t, req, http.StatusOK)
reviewsCountCheck(t,
"check we have one valid approval",
pullIssue.ID, user8.ID, 0, 0, 1, true)
// emulate of auto-dismiss lgtm on a protected branch that where a pull just got an update
_, err := db.GetEngine(t.Context()).Where("issue_id = ? AND reviewer_id = ?", pullIssue.ID, user8.ID).
Cols("dismissed").Update(&issues_model.Review{Dismissed: true})
assert.NoError(t, err)
// user2 request user8 again
req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", repo.OwnerName, repo.Name, pullIssue.Index), &api.PullReviewRequestOptions{
Reviewers: []string{user8.LoginName},
}).AddTokenAuth(token2)
MakeRequest(t, req, http.StatusCreated)
reviewsCountCheck(t,
"check we have no valid approval and one review request",
pullIssue.ID, user8.ID, 1, 1, 2, false)
// user8 dismiss review
permUser8, err := access_model.GetIndividualUserRepoPermission(t.Context(), pullIssue.Repo, user8)
assert.NoError(t, err)
_, err = issue_service.ReviewRequest(t.Context(), pullIssue, user8, &permUser8, user8, false)
assert.NoError(t, err)
reviewsCountCheck(t,
"check new review request is now dismissed",
pullIssue.ID, user8.ID, 1, 0, 1, false)
// add a new valid approval
req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews", repo.OwnerName, repo.Name, pullIssue.Index), &api.CreatePullReviewOptions{
Event: "APPROVED",
Body: "lgtm",
}).AddTokenAuth(token8)
MakeRequest(t, req, http.StatusOK)
reviewsCountCheck(t,
"check that old reviews requests are deleted",
pullIssue.ID, user8.ID, 1, 0, 2, true)
// now add a change request witch should dismiss the approval
req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews", repo.OwnerName, repo.Name, pullIssue.Index), &api.CreatePullReviewOptions{
Event: "REQUEST_CHANGES",
Body: "please change XYZ",
}).AddTokenAuth(token8)
MakeRequest(t, req, http.StatusOK)
reviewsCountCheck(t,
"check that old reviews are dismissed",
pullIssue.ID, user8.ID, 2, 0, 3, false)
}
func testAPIPullReviewCommentReply(t *testing.T) {
pullIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3})
require.NoError(t, pullIssue.LoadRepo(t.Context()))
require.NoError(t, pullIssue.LoadPullRequest(t.Context()))
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
gitRepo, err := git.OpenRepository(t.Context(), pullIssue.Repo)
require.NoError(t, err)
defer gitRepo.Close()
commitID, err := gitRepo.GetRefCommitID(t.Context(), pullIssue.PullRequest.GetGitHeadRefName())
require.NoError(t, err)
parent, err := pull_service.CreateCodeComment(t.Context(), doer, gitRepo, pullIssue, 1, "parent comment", "README.md", false, 0, commitID, nil)
require.NoError(t, err)
require.NotZero(t, parent.ReviewID)
repo := pullIssue.Repo
session := loginUser(t, doer.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
url := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/comments/%d/replies", repo.OwnerName, repo.Name, pullIssue.Index, parent.ID)
// happy path
req := NewRequestWithJSON(t, http.MethodPost, url, &api.CreatePullReviewCommentReplyOptions{Body: "the reply"}).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusCreated)
reply := DecodeJSON(t, resp, &api.PullReviewComment{})
assert.Equal(t, "the reply", reply.Body)
assert.Equal(t, parent.ReviewID, reply.ReviewID)
assert.Equal(t, "README.md", reply.Path)
// empty body — caught by binding
req = NewRequestWithJSON(t, http.MethodPost, url, &api.CreatePullReviewCommentReplyOptions{}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusUnprocessableEntity)
// reply to a non-existent comment
bad := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/comments/%d/replies", repo.OwnerName, repo.Name, pullIssue.Index, 999999)
req = NewRequestWithJSON(t, http.MethodPost, bad, &api.CreatePullReviewCommentReplyOptions{Body: "x"}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
// reply to a code comment that belongs to a different PR — 404
otherCodeComment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 4, Type: issues_model.CommentTypeCode})
require.NotEqual(t, pullIssue.ID, otherCodeComment.IssueID)
wrongPR := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/comments/%d/replies", repo.OwnerName, repo.Name, pullIssue.Index, otherCodeComment.ID)
req = NewRequestWithJSON(t, http.MethodPost, wrongPR, &api.CreatePullReviewCommentReplyOptions{Body: "x"}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
}
func reviewsCountCheck(t *testing.T, name string, issueID, reviewerID int64, expectedDismissed, expectedRequested, expectedTotal int, expectApproval bool) {
t.Run(name, func(t *testing.T) {
unittest.AssertCountByCond(t, "review", builder.Eq{
"issue_id": issueID,
"reviewer_id": reviewerID,
"dismissed": true,
}, expectedDismissed)
unittest.AssertCountByCond(t, "review", builder.Eq{
"issue_id": issueID,
"reviewer_id": reviewerID,
}, expectedTotal)
unittest.AssertCountByCond(t, "review", builder.Eq{
"issue_id": issueID,
"reviewer_id": reviewerID,
"type": issues_model.ReviewTypeRequest,
}, expectedRequested)
approvalCount := 0
if expectApproval {
approvalCount = 1
}
unittest.AssertCountByCond(t, "review", builder.Eq{
"issue_id": issueID,
"reviewer_id": reviewerID,
"type": issues_model.ReviewTypeApprove,
"dismissed": false,
}, approvalCount)
})
}