1// Copyright 2020 The Gitea Authors. All rights reserved.
2// Use of this source code is governed by a MIT-style
3// license that can be found in the LICENSE file.
4
5package integrations
6
7import (
8	"fmt"
9	"net/http"
10	"testing"
11
12	"code.gitea.io/gitea/models"
13	repo_model "code.gitea.io/gitea/models/repo"
14	"code.gitea.io/gitea/models/unittest"
15	"code.gitea.io/gitea/modules/json"
16	api "code.gitea.io/gitea/modules/structs"
17
18	"github.com/stretchr/testify/assert"
19)
20
21func TestAPIPullReview(t *testing.T) {
22	defer prepareTestEnv(t)()
23	pullIssue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 3}).(*models.Issue)
24	assert.NoError(t, pullIssue.LoadAttributes())
25	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pullIssue.RepoID}).(*repo_model.Repository)
26
27	// test ListPullReviews
28	session := loginUser(t, "user2")
29	token := getTokenForLoggedInUser(t, session)
30	req := NewRequestf(t, http.MethodGet, "/api/v1/repos/%s/%s/pulls/%d/reviews?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, token)
31	resp := session.MakeRequest(t, req, http.StatusOK)
32
33	var reviews []*api.PullReview
34	DecodeJSON(t, resp, &reviews)
35	if !assert.Len(t, reviews, 6) {
36		return
37	}
38	for _, r := range reviews {
39		assert.EqualValues(t, pullIssue.HTMLURL(), r.HTMLPullURL)
40	}
41	assert.EqualValues(t, 8, reviews[3].ID)
42	assert.EqualValues(t, "APPROVED", reviews[3].State)
43	assert.EqualValues(t, 0, reviews[3].CodeCommentsCount)
44	assert.True(t, reviews[3].Stale)
45	assert.False(t, reviews[3].Official)
46
47	assert.EqualValues(t, 10, reviews[5].ID)
48	assert.EqualValues(t, "REQUEST_CHANGES", reviews[5].State)
49	assert.EqualValues(t, 1, reviews[5].CodeCommentsCount)
50	assert.EqualValues(t, -1, reviews[5].Reviewer.ID) // ghost user
51	assert.False(t, reviews[5].Stale)
52	assert.True(t, reviews[5].Official)
53
54	// test GetPullReview
55	req = NewRequestf(t, http.MethodGet, "/api/v1/repos/%s/%s/pulls/%d/reviews/%d?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, reviews[3].ID, token)
56	resp = session.MakeRequest(t, req, http.StatusOK)
57	var review api.PullReview
58	DecodeJSON(t, resp, &review)
59	assert.EqualValues(t, *reviews[3], review)
60
61	req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/pulls/%d/reviews/%d?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, reviews[5].ID, token)
62	resp = session.MakeRequest(t, req, http.StatusOK)
63	DecodeJSON(t, resp, &review)
64	assert.EqualValues(t, *reviews[5], review)
65
66	// test GetPullReviewComments
67	comment := unittest.AssertExistsAndLoadBean(t, &models.Comment{ID: 7}).(*models.Comment)
68	req = NewRequestf(t, http.MethodGet, "/api/v1/repos/%s/%s/pulls/%d/reviews/%d/comments?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, 10, token)
69	resp = session.MakeRequest(t, req, http.StatusOK)
70	var reviewComments []*api.PullReviewComment
71	DecodeJSON(t, resp, &reviewComments)
72	assert.Len(t, reviewComments, 1)
73	assert.EqualValues(t, "Ghost", reviewComments[0].Poster.UserName)
74	assert.EqualValues(t, "a review from a deleted user", reviewComments[0].Body)
75	assert.EqualValues(t, comment.ID, reviewComments[0].ID)
76	assert.EqualValues(t, comment.UpdatedUnix, reviewComments[0].Updated.Unix())
77	assert.EqualValues(t, comment.HTMLURL(), reviewComments[0].HTMLURL)
78
79	// test CreatePullReview
80	req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, token), &api.CreatePullReviewOptions{
81		Body: "body1",
82		// Event: "" # will result in PENDING
83		Comments: []api.CreatePullReviewComment{{
84			Path:       "README.md",
85			Body:       "first new line",
86			OldLineNum: 0,
87			NewLineNum: 1,
88		}, {
89			Path:       "README.md",
90			Body:       "first old line",
91			OldLineNum: 1,
92			NewLineNum: 0,
93		}, {
94			Path:       "iso-8859-1.txt",
95			Body:       "this line contains a non-utf-8 character",
96			OldLineNum: 0,
97			NewLineNum: 1,
98		},
99		},
100	})
101	resp = session.MakeRequest(t, req, http.StatusOK)
102	DecodeJSON(t, resp, &review)
103	assert.EqualValues(t, 6, review.ID)
104	assert.EqualValues(t, "PENDING", review.State)
105	assert.EqualValues(t, 3, review.CodeCommentsCount)
106
107	// test SubmitPullReview
108	req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews/%d?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, review.ID, token), &api.SubmitPullReviewOptions{
109		Event: "APPROVED",
110		Body:  "just two nits",
111	})
112	resp = session.MakeRequest(t, req, http.StatusOK)
113	DecodeJSON(t, resp, &review)
114	assert.EqualValues(t, 6, review.ID)
115	assert.EqualValues(t, "APPROVED", review.State)
116	assert.EqualValues(t, 3, review.CodeCommentsCount)
117
118	// test dismiss review
119	req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews/%d/dismissals?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, review.ID, token), &api.DismissPullReviewOptions{
120		Message: "test",
121	})
122	resp = session.MakeRequest(t, req, http.StatusOK)
123	DecodeJSON(t, resp, &review)
124	assert.EqualValues(t, 6, review.ID)
125	assert.True(t, review.Dismissed)
126
127	// test dismiss review
128	req = NewRequest(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews/%d/undismissals?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, review.ID, token))
129	resp = session.MakeRequest(t, req, http.StatusOK)
130	DecodeJSON(t, resp, &review)
131	assert.EqualValues(t, 6, review.ID)
132	assert.False(t, review.Dismissed)
133
134	// test DeletePullReview
135	req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, token), &api.CreatePullReviewOptions{
136		Body:  "just a comment",
137		Event: "COMMENT",
138	})
139	resp = session.MakeRequest(t, req, http.StatusOK)
140	DecodeJSON(t, resp, &review)
141	assert.EqualValues(t, "COMMENT", review.State)
142	assert.EqualValues(t, 0, review.CodeCommentsCount)
143	req = NewRequestf(t, http.MethodDelete, "/api/v1/repos/%s/%s/pulls/%d/reviews/%d?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, review.ID, token)
144	session.MakeRequest(t, req, http.StatusNoContent)
145
146	// test CreatePullReview Comment without body but with comments
147	req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, token), &api.CreatePullReviewOptions{
148		// Body:  "",
149		Event: "COMMENT",
150		Comments: []api.CreatePullReviewComment{{
151			Path:       "README.md",
152			Body:       "first new line",
153			OldLineNum: 0,
154			NewLineNum: 1,
155		}, {
156			Path:       "README.md",
157			Body:       "first old line",
158			OldLineNum: 1,
159			NewLineNum: 0,
160		},
161		},
162	})
163	var commentReview api.PullReview
164
165	resp = session.MakeRequest(t, req, http.StatusOK)
166	DecodeJSON(t, resp, &commentReview)
167	assert.EqualValues(t, "COMMENT", commentReview.State)
168	assert.EqualValues(t, 2, commentReview.CodeCommentsCount)
169	assert.EqualValues(t, "", commentReview.Body)
170	assert.EqualValues(t, false, commentReview.Dismissed)
171
172	// test CreatePullReview Comment with body but without comments
173	commentBody := "This is a body of the comment."
174	req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, token), &api.CreatePullReviewOptions{
175		Body:     commentBody,
176		Event:    "COMMENT",
177		Comments: []api.CreatePullReviewComment{},
178	})
179
180	resp = session.MakeRequest(t, req, http.StatusOK)
181	DecodeJSON(t, resp, &commentReview)
182	assert.EqualValues(t, "COMMENT", commentReview.State)
183	assert.EqualValues(t, 0, commentReview.CodeCommentsCount)
184	assert.EqualValues(t, commentBody, commentReview.Body)
185	assert.EqualValues(t, false, commentReview.Dismissed)
186
187	// test CreatePullReview Comment without body and no comments
188	req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, token), &api.CreatePullReviewOptions{
189		Body:     "",
190		Event:    "COMMENT",
191		Comments: []api.CreatePullReviewComment{},
192	})
193	resp = session.MakeRequest(t, req, http.StatusUnprocessableEntity)
194	errMap := make(map[string]interface{})
195	json.Unmarshal(resp.Body.Bytes(), &errMap)
196	assert.EqualValues(t, "review event COMMENT requires a body or a comment", errMap["message"].(string))
197
198	// test get review requests
199	// to make it simple, use same api with get review
200	pullIssue12 := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 12}).(*models.Issue)
201	assert.NoError(t, pullIssue12.LoadAttributes())
202	repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pullIssue12.RepoID}).(*repo_model.Repository)
203
204	req = NewRequestf(t, http.MethodGet, "/api/v1/repos/%s/%s/pulls/%d/reviews?token=%s", repo3.OwnerName, repo3.Name, pullIssue12.Index, token)
205	resp = session.MakeRequest(t, req, http.StatusOK)
206	DecodeJSON(t, resp, &reviews)
207	assert.EqualValues(t, 11, reviews[0].ID)
208	assert.EqualValues(t, "REQUEST_REVIEW", reviews[0].State)
209	assert.EqualValues(t, 0, reviews[0].CodeCommentsCount)
210	assert.False(t, reviews[0].Stale)
211	assert.True(t, reviews[0].Official)
212	assert.EqualValues(t, "test_team", reviews[0].ReviewerTeam.Name)
213
214	assert.EqualValues(t, 12, reviews[1].ID)
215	assert.EqualValues(t, "REQUEST_REVIEW", reviews[1].State)
216	assert.EqualValues(t, 0, reviews[0].CodeCommentsCount)
217	assert.False(t, reviews[1].Stale)
218	assert.True(t, reviews[1].Official)
219	assert.EqualValues(t, 1, reviews[1].Reviewer.ID)
220}
221
222func TestAPIPullReviewRequest(t *testing.T) {
223	defer prepareTestEnv(t)()
224	pullIssue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 3}).(*models.Issue)
225	assert.NoError(t, pullIssue.LoadAttributes())
226	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pullIssue.RepoID}).(*repo_model.Repository)
227
228	// Test add Review Request
229	session := loginUser(t, "user2")
230	token := getTokenForLoggedInUser(t, session)
231	req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, token), &api.PullReviewRequestOptions{
232		Reviewers: []string{"user4@example.com", "user8"},
233	})
234	session.MakeRequest(t, req, http.StatusCreated)
235
236	// poster of pr can't be reviewer
237	req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, token), &api.PullReviewRequestOptions{
238		Reviewers: []string{"user1"},
239	})
240	session.MakeRequest(t, req, http.StatusUnprocessableEntity)
241
242	// test user not exist
243	req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, token), &api.PullReviewRequestOptions{
244		Reviewers: []string{"testOther"},
245	})
246	session.MakeRequest(t, req, http.StatusNotFound)
247
248	// Test Remove Review Request
249	session2 := loginUser(t, "user4")
250	token2 := getTokenForLoggedInUser(t, session2)
251
252	req = NewRequestWithJSON(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, token2), &api.PullReviewRequestOptions{
253		Reviewers: []string{"user4"},
254	})
255	session.MakeRequest(t, req, http.StatusNoContent)
256
257	// doer is not admin
258	req = NewRequestWithJSON(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, token2), &api.PullReviewRequestOptions{
259		Reviewers: []string{"user8"},
260	})
261	session.MakeRequest(t, req, http.StatusUnprocessableEntity)
262
263	req = NewRequestWithJSON(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, token), &api.PullReviewRequestOptions{
264		Reviewers: []string{"user8"},
265	})
266	session.MakeRequest(t, req, http.StatusNoContent)
267
268	// Test team review request
269	pullIssue12 := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 12}).(*models.Issue)
270	assert.NoError(t, pullIssue12.LoadAttributes())
271	repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pullIssue12.RepoID}).(*repo_model.Repository)
272
273	// Test add Team Review Request
274	req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers?token=%s", repo3.OwnerName, repo3.Name, pullIssue12.Index, token), &api.PullReviewRequestOptions{
275		TeamReviewers: []string{"team1", "owners"},
276	})
277	session.MakeRequest(t, req, http.StatusCreated)
278
279	// Test add Team Review Request to not allowned
280	req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers?token=%s", repo3.OwnerName, repo3.Name, pullIssue12.Index, token), &api.PullReviewRequestOptions{
281		TeamReviewers: []string{"test_team"},
282	})
283	session.MakeRequest(t, req, http.StatusUnprocessableEntity)
284
285	// Test add Team Review Request to not exist
286	req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers?token=%s", repo3.OwnerName, repo3.Name, pullIssue12.Index, token), &api.PullReviewRequestOptions{
287		TeamReviewers: []string{"not_exist_team"},
288	})
289	session.MakeRequest(t, req, http.StatusNotFound)
290
291	// Test Remove team Review Request
292	req = NewRequestWithJSON(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers?token=%s", repo3.OwnerName, repo3.Name, pullIssue12.Index, token), &api.PullReviewRequestOptions{
293		TeamReviewers: []string{"team1"},
294	})
295	session.MakeRequest(t, req, http.StatusNoContent)
296
297	// empty request test
298	req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers?token=%s", repo3.OwnerName, repo3.Name, pullIssue12.Index, token), &api.PullReviewRequestOptions{})
299	session.MakeRequest(t, req, http.StatusCreated)
300
301	req = NewRequestWithJSON(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers?token=%s", repo3.OwnerName, repo3.Name, pullIssue12.Index, token), &api.PullReviewRequestOptions{})
302	session.MakeRequest(t, req, http.StatusNoContent)
303}
304