Files
Gitea/tests/integration/api_pull_review_test.go
T
silverwindandGiteabot d58be0bcff fix(pull): keep review body in sync when its comment is edited (#40058)
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: Giteabot <teabot@gitea.io>
2026-10-31 17:31:39 +01:00

648 lines
29 KiB
Go

// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"fmt"
"net/http"
"strings"
"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)()
// TODO add tests for already merged PR and closed PR
t.Run("Commits", func(t *testing.T) {
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
assert.NoError(t, pr.LoadIssue(t.Context()))
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pr.HeadRepoID})
req := NewRequestf(t, http.MethodGet, "/api/v1/repos/%s/%s/pulls/%d/commits", repo.OwnerName, repo.Name, pr.Index)
resp := MakeRequest(t, req, http.StatusOK)
commits := DecodeJSON(t, resp, []*api.Commit{})
require.Len(t, commits, 2)
assert.Equal(t, "985f0301dba5e7b34be866819cd15ad3d8f508ee", commits[0].SHA)
assert.Equal(t, "5c050d3b6d2db231ab1f64e324f1b6b9a0b181c2", commits[1].SHA)
assert.NotEmpty(t, commits[0].Files)
assert.NotEmpty(t, commits[1].Files)
assert.NotNil(t, commits[0].RepoCommit.Verification)
assert.NotNil(t, commits[1].RepoCommit.Verification)
})
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"])
unknownCommitID := strings.Repeat("a", 40)
req = NewRequestWithJSON(t, http.MethodPost, reviewsURL, &api.CreatePullReviewOptions{
Event: api.ReviewStateComment,
CommitID: unknownCommitID,
Comments: []api.CreatePullReviewComment{{Path: "README.md", Body: "inline", NewLineNum: 1}},
}).AddTokenAuth(token)
review = DecodeJSON(t, MakeRequest(t, req, http.StatusOK), &api.PullReview{})
assert.Equal(t, unknownCommitID, review.CommitID)
unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ReviewID: review.ID, Type: issues_model.CommentTypeCode, Content: "inline"})
// 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})
requestReview := func(method string, repo *repo_model.Repository, index int64, token string, options *api.PullReviewRequestOptions, expectedStatus int) {
req := NewRequestWithJSON(t, method, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", repo.OwnerName, repo.Name, index), options).AddTokenAuth(token)
MakeRequest(t, req, expectedStatus)
}
// Test add Review Request
session := loginUser(t, "user2")
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
requestReview(http.MethodPost, repo, pullIssue.Index, token, &api.PullReviewRequestOptions{
Reviewers: []string{"user4@example.com", "user8"},
}, http.StatusCreated)
// poster of pr can't be reviewer
requestReview(http.MethodPost, repo, pullIssue.Index, token, &api.PullReviewRequestOptions{
Reviewers: []string{"user1"},
}, http.StatusUnprocessableEntity)
// test user not exist
requestReview(http.MethodPost, repo, pullIssue.Index, token, &api.PullReviewRequestOptions{
Reviewers: []string{"testOther"},
}, http.StatusNotFound)
// Test Remove Review Request
session2 := loginUser(t, "user4")
token2 := getTokenForLoggedInUser(t, session2, auth_model.AccessTokenScopeWriteRepository)
requestReview(http.MethodDelete, repo, pullIssue.Index, token2, &api.PullReviewRequestOptions{
Reviewers: []string{"user4"},
}, http.StatusNoContent)
// doer is not admin
requestReview(http.MethodDelete, repo, pullIssue.Index, token2, &api.PullReviewRequestOptions{
Reviewers: []string{"user8"},
}, http.StatusUnprocessableEntity)
requestReview(http.MethodDelete, repo, pullIssue.Index, token, &api.PullReviewRequestOptions{
Reviewers: []string{"user8"},
}, 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)
requestReview(http.MethodPost, pull21Repo, pullIssue21.Index, user38Token, &api.PullReviewRequestOptions{
Reviewers: []string{"user4@example.com"},
}, http.StatusCreated)
requestReview(http.MethodDelete, pull21Repo, pullIssue21.Index, user38Token, &api.PullReviewRequestOptions{
Reviewers: []string{"user4@example.com"},
}, http.StatusNoContent)
// the poster of the PR can add/remove a review request
user39Session := loginUser(t, "user39")
user39Token := getTokenForLoggedInUser(t, user39Session, auth_model.AccessTokenScopeWriteRepository)
requestReview(http.MethodPost, pull21Repo, pullIssue21.Index, user39Token, &api.PullReviewRequestOptions{
Reviewers: []string{"user8"},
}, http.StatusCreated)
requestReview(http.MethodDelete, pull21Repo, pullIssue21.Index, user39Token, &api.PullReviewRequestOptions{
Reviewers: []string{"user8"},
}, 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
requestReview(http.MethodPost, pull22Repo, pullIssue22.Index, user39Token, &api.PullReviewRequestOptions{
Reviewers: []string{"user38"},
}, http.StatusCreated) // user39 is from a team with read permission on pull requests unit
requestReview(http.MethodDelete, pull22Repo, pullIssue22.Index, user39Token, &api.PullReviewRequestOptions{
Reviewers: []string{"user38"},
}, http.StatusNoContent) // user39 is from a team with read permission on pull requests unit
// 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
requestReview(http.MethodPost, repo3, pullIssue12.Index, token, &api.PullReviewRequestOptions{
TeamReviewers: []string{"team1", "owners"},
}, http.StatusCreated)
// Test add Team Review Request to not allowned
requestReview(http.MethodPost, repo3, pullIssue12.Index, token, &api.PullReviewRequestOptions{
TeamReviewers: []string{"test_team"},
}, http.StatusUnprocessableEntity)
// Test add Team Review Request to not exist
requestReview(http.MethodPost, repo3, pullIssue12.Index, token, &api.PullReviewRequestOptions{
TeamReviewers: []string{"not_exist_team"},
}, http.StatusNotFound)
// Test Remove team Review Request
requestReview(http.MethodDelete, repo3, pullIssue12.Index, token, &api.PullReviewRequestOptions{
TeamReviewers: []string{"team1"},
}, http.StatusNoContent)
// empty request test
requestReview(http.MethodPost, repo3, pullIssue12.Index, token, &api.PullReviewRequestOptions{}, http.StatusCreated)
requestReview(http.MethodDelete, repo3, pullIssue12.Index, token, &api.PullReviewRequestOptions{}, 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
for _, name := range []string{
"check we have only one review request",
// user2 request user8 again, it is expected to be ignored
"check we have only one review request, even after re-request it 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, name, 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)
})
}