loading up the forgejo repo on tangled to test page performance
1// Copyright 2017 The Gitea Authors. All rights reserved.
2// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved.
3// SPDX-License-Identifier: MIT
4
5package integration
6
7import (
8 "fmt"
9 "net/http"
10 "net/url"
11 "path"
12 "regexp"
13 "strconv"
14 "strings"
15 "testing"
16 "time"
17
18 auth_model "forgejo.org/models/auth"
19 "forgejo.org/models/db"
20 issues_model "forgejo.org/models/issues"
21 project_model "forgejo.org/models/project"
22 repo_model "forgejo.org/models/repo"
23 unit_model "forgejo.org/models/unit"
24 "forgejo.org/models/unittest"
25 user_model "forgejo.org/models/user"
26 "forgejo.org/modules/indexer/issues"
27 "forgejo.org/modules/optional"
28 "forgejo.org/modules/references"
29 "forgejo.org/modules/setting"
30 api "forgejo.org/modules/structs"
31 "forgejo.org/modules/test"
32 files_service "forgejo.org/services/repository/files"
33 "forgejo.org/tests"
34
35 "github.com/PuerkitoBio/goquery"
36 "github.com/stretchr/testify/assert"
37 "github.com/stretchr/testify/require"
38)
39
40func getIssuesSelection(t testing.TB, htmlDoc *HTMLDoc) *goquery.Selection {
41 issueList := htmlDoc.doc.Find("#issue-list")
42 assert.Equal(t, 1, issueList.Length())
43 return issueList.Find(".flex-item").Find(".issue-title")
44}
45
46func getIssue(t *testing.T, repoID int64, issueSelection *goquery.Selection) *issues_model.Issue {
47 href, exists := issueSelection.Attr("href")
48 assert.True(t, exists)
49 indexStr := href[strings.LastIndexByte(href, '/')+1:]
50 index, err := strconv.Atoi(indexStr)
51 require.NoError(t, err, "Invalid issue href: %s", href)
52 return unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: repoID, Index: int64(index)})
53}
54
55func assertMatch(t testing.TB, issue *issues_model.Issue, keyword string) {
56 matches := strings.Contains(strings.ToLower(issue.Title), keyword) ||
57 strings.Contains(strings.ToLower(issue.Content), keyword)
58 for _, comment := range issue.Comments {
59 matches = matches || strings.Contains(
60 strings.ToLower(comment.Content),
61 keyword,
62 )
63 }
64 assert.True(t, matches)
65}
66
67func TestNoLoginViewIssues(t *testing.T) {
68 defer tests.PrepareTestEnv(t)()
69
70 req := NewRequest(t, "GET", "/user2/repo1/issues")
71 MakeRequest(t, req, http.StatusOK)
72}
73
74func TestViewIssues(t *testing.T) {
75 defer tests.PrepareTestEnv(t)()
76
77 req := NewRequest(t, "GET", "/user2/repo1/issues")
78 resp := MakeRequest(t, req, http.StatusOK)
79
80 htmlDoc := NewHTMLParser(t, resp.Body)
81 search := htmlDoc.doc.Find(".list-header-search > .search > .input > input")
82 placeholder, _ := search.Attr("placeholder")
83 assert.Equal(t, "Search issues…", placeholder)
84}
85
86func TestViewIssuesSortByType(t *testing.T) {
87 defer tests.PrepareTestEnv(t)()
88
89 user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
90 repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
91
92 session := loginUser(t, user.Name)
93 req := NewRequest(t, "GET", repo.Link()+"/issues?type=created_by")
94 resp := session.MakeRequest(t, req, http.StatusOK)
95
96 htmlDoc := NewHTMLParser(t, resp.Body)
97 issuesSelection := getIssuesSelection(t, htmlDoc)
98 expectedNumIssues := unittest.GetCount(t,
99 &issues_model.Issue{RepoID: repo.ID, PosterID: user.ID},
100 unittest.Cond("is_closed=?", false),
101 unittest.Cond("is_pull=?", false),
102 )
103 if expectedNumIssues > setting.UI.IssuePagingNum {
104 expectedNumIssues = setting.UI.IssuePagingNum
105 }
106 assert.Equal(t, expectedNumIssues, issuesSelection.Length())
107
108 issuesSelection.Each(func(_ int, selection *goquery.Selection) {
109 issue := getIssue(t, repo.ID, selection)
110 assert.Equal(t, user.ID, issue.PosterID)
111 })
112}
113
114func TestViewIssuesKeyword(t *testing.T) {
115 defer tests.PrepareTestEnv(t)()
116
117 repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
118 issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{
119 RepoID: repo.ID,
120 Index: 1,
121 })
122 issues.UpdateIssueIndexer(t.Context(), issue.ID)
123 time.Sleep(time.Second * 1)
124
125 const keyword = "first"
126 req := NewRequestf(t, "GET", "%s/issues?q=%s", repo.Link(), keyword)
127 resp := MakeRequest(t, req, http.StatusOK)
128
129 htmlDoc := NewHTMLParser(t, resp.Body)
130 issuesSelection := getIssuesSelection(t, htmlDoc)
131 assert.Equal(t, 1, issuesSelection.Length())
132 issuesSelection.Each(func(_ int, selection *goquery.Selection) {
133 issue := getIssue(t, repo.ID, selection)
134 assert.False(t, issue.IsClosed)
135 assert.False(t, issue.IsPull)
136 assertMatch(t, issue, keyword)
137 })
138
139 // keyword: 'firstt'
140 // should not match when using phrase search
141 req = NewRequestf(t, "GET", "%s/issues?q=\"%st\"", repo.Link(), keyword)
142 resp = MakeRequest(t, req, http.StatusOK)
143 htmlDoc = NewHTMLParser(t, resp.Body)
144 issuesSelection = getIssuesSelection(t, htmlDoc)
145 assert.Equal(t, 0, issuesSelection.Length())
146
147 // should match as 'first' when using a standard query
148 req = NewRequestf(t, "GET", "%s/issues?q=%st", repo.Link(), keyword)
149 resp = MakeRequest(t, req, http.StatusOK)
150 htmlDoc = NewHTMLParser(t, resp.Body)
151 issuesSelection = getIssuesSelection(t, htmlDoc)
152 assert.Equal(t, 1, issuesSelection.Length())
153 issuesSelection.Each(func(_ int, selection *goquery.Selection) {
154 issue := getIssue(t, repo.ID, selection)
155 assert.False(t, issue.IsClosed)
156 assert.False(t, issue.IsPull)
157 assertMatch(t, issue, keyword)
158 })
159}
160
161func TestViewIssuesSearchOptions(t *testing.T) {
162 defer tests.PrepareTestEnv(t)()
163
164 repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
165
166 // there are two issues in repo1, both bound to a project. Add one
167 // that is not bound to any project.
168 _, issueNoProject := testIssueWithBean(t, "user2", 1, "Title", "Description")
169
170 t.Run("All issues", func(t *testing.T) {
171 req := NewRequestf(t, "GET", "%s/issues?state=all", repo.Link())
172 resp := MakeRequest(t, req, http.StatusOK)
173 htmlDoc := NewHTMLParser(t, resp.Body)
174 issuesSelection := getIssuesSelection(t, htmlDoc)
175 assert.Equal(t, 3, issuesSelection.Length())
176 })
177
178 t.Run("Issues with no project", func(t *testing.T) {
179 req := NewRequestf(t, "GET", "%s/issues?state=all&project=-1", repo.Link())
180 resp := MakeRequest(t, req, http.StatusOK)
181 htmlDoc := NewHTMLParser(t, resp.Body)
182 issuesSelection := getIssuesSelection(t, htmlDoc)
183 assert.Equal(t, 1, issuesSelection.Length())
184 issuesSelection.Each(func(_ int, selection *goquery.Selection) {
185 issue := getIssue(t, repo.ID, selection)
186 assert.Equal(t, issueNoProject.ID, issue.ID)
187 })
188 })
189
190 t.Run("Issues with a specific project", func(t *testing.T) {
191 project := unittest.AssertExistsAndLoadBean(t, &project_model.Project{ID: 1})
192
193 req := NewRequestf(t, "GET", "%s/issues?state=all&project=%d", repo.Link(), project.ID)
194 resp := MakeRequest(t, req, http.StatusOK)
195 htmlDoc := NewHTMLParser(t, resp.Body)
196 issuesSelection := getIssuesSelection(t, htmlDoc)
197 assert.Equal(t, 2, issuesSelection.Length())
198 found := map[int64]bool{
199 1: false,
200 5: false,
201 }
202 issuesSelection.Each(func(_ int, selection *goquery.Selection) {
203 issue := getIssue(t, repo.ID, selection)
204 found[issue.ID] = true
205 })
206 assert.Len(t, found, 2)
207 assert.True(t, found[1])
208 assert.True(t, found[5])
209 })
210}
211
212func TestNoLoginViewIssue(t *testing.T) {
213 defer tests.PrepareTestEnv(t)()
214
215 req := NewRequest(t, "GET", "/user2/repo1/issues/1")
216 MakeRequest(t, req, http.StatusOK)
217}
218
219func testNewIssue(t *testing.T, session *TestSession, user, repo, title, content string) string {
220 req := NewRequest(t, "GET", path.Join(user, repo, "issues", "new"))
221 resp := session.MakeRequest(t, req, http.StatusOK)
222
223 htmlDoc := NewHTMLParser(t, resp.Body)
224 link, exists := htmlDoc.doc.Find("form.ui.form").Attr("action")
225 assert.True(t, exists, "The template has changed")
226 req = NewRequestWithValues(t, "POST", link, map[string]string{
227 "_csrf": htmlDoc.GetCSRF(),
228 "title": title,
229 "content": content,
230 })
231 resp = session.MakeRequest(t, req, http.StatusOK)
232
233 issueURL := test.RedirectURL(resp)
234 req = NewRequest(t, "GET", issueURL)
235 resp = session.MakeRequest(t, req, http.StatusOK)
236
237 htmlDoc = NewHTMLParser(t, resp.Body)
238 val := htmlDoc.doc.Find("#issue-title-display").Text()
239 assert.Contains(t, val, title)
240 // test for first line only and if it contains only letters and spaces
241 contentFirstLine := strings.Split(content, "\n")[0]
242 patNotLetterOrSpace := regexp.MustCompile(`[^\p{L}\s]`)
243 if len(contentFirstLine) != 0 && !patNotLetterOrSpace.MatchString(contentFirstLine) {
244 val = htmlDoc.doc.Find(".comment .render-content p").First().Text()
245 assert.Equal(t, contentFirstLine, val)
246 }
247 return issueURL
248}
249
250func testIssueAddComment(t *testing.T, session *TestSession, issueURL, content, status string) int64 {
251 req := NewRequest(t, "GET", issueURL)
252 resp := session.MakeRequest(t, req, http.StatusOK)
253
254 htmlDoc := NewHTMLParser(t, resp.Body)
255 link, exists := htmlDoc.doc.Find("#comment-form").Attr("action")
256 assert.True(t, exists, "The template has changed")
257
258 commentCount := htmlDoc.doc.Find(".comment-list .comment .render-content").Length()
259
260 req = NewRequestWithValues(t, "POST", link, map[string]string{
261 "_csrf": htmlDoc.GetCSRF(),
262 "content": content,
263 "status": status,
264 })
265 resp = session.MakeRequest(t, req, http.StatusOK)
266
267 req = NewRequest(t, "GET", test.RedirectURL(resp))
268 resp = session.MakeRequest(t, req, http.StatusOK)
269
270 htmlDoc = NewHTMLParser(t, resp.Body)
271
272 val := htmlDoc.doc.Find(".comment-list .comment .render-content p").Eq(commentCount).Text()
273 assert.Equal(t, content, val)
274
275 idAttr, has := htmlDoc.doc.Find(".comment-list .comment").Eq(commentCount).Attr("id")
276 idStr := idAttr[strings.LastIndexByte(idAttr, '-')+1:]
277 assert.True(t, has)
278 id, err := strconv.Atoi(idStr)
279 require.NoError(t, err)
280 return int64(id)
281}
282
283func TestNewIssue(t *testing.T) {
284 defer tests.PrepareTestEnv(t)()
285 session := loginUser(t, "user2")
286 testNewIssue(t, session, "user2", "repo1", "Title", "Description")
287}
288
289func TestIssueCheckboxes(t *testing.T) {
290 defer tests.PrepareTestEnv(t)()
291 session := loginUser(t, "user2")
292 issueURL := testNewIssue(t, session, "user2", "repo1", "Title", `- [x] small x
293- [X] capital X
294- [ ] empty
295 - [x]x without gap
296 - [ ]empty without gap
297- [x]
298x on new line
299- [ ]
300empty on new line
301 - [ ] tabs instead of spaces
302Description`)
303 req := NewRequest(t, "GET", issueURL)
304 resp := session.MakeRequest(t, req, http.StatusOK)
305 issueContent := NewHTMLParser(t, resp.Body).doc.Find(".comment .render-content").First()
306 isCheckBox := func(i int, s *goquery.Selection) bool {
307 typeVal, typeExists := s.Attr("type")
308 return typeExists && typeVal == "checkbox"
309 }
310 isChecked := func(i int, s *goquery.Selection) bool {
311 _, checkedExists := s.Attr("checked")
312 return checkedExists
313 }
314 checkBoxes := issueContent.Find("input").FilterFunction(isCheckBox)
315 assert.Equal(t, 8, checkBoxes.Length())
316 assert.Equal(t, 4, checkBoxes.FilterFunction(isChecked).Length())
317
318 // Issues list should show the correct numbers of checked and total checkboxes
319 repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, "user2", "repo1")
320 require.NoError(t, err)
321 req = NewRequestf(t, "GET", "%s/issues", repo.Link())
322 resp = MakeRequest(t, req, http.StatusOK)
323
324 htmlDoc := NewHTMLParser(t, resp.Body)
325 issuesSelection := htmlDoc.Find("#issue-list .flex-item")
326 assert.Equal(t, "4 / 8", strings.TrimSpace(issuesSelection.Find(".checklist").Text()))
327 value, _ := issuesSelection.Find("progress").Attr("value")
328 vmax, _ := issuesSelection.Find("progress").Attr("max")
329 assert.Equal(t, "4", value)
330 assert.Equal(t, "8", vmax)
331}
332
333func TestIssueDependencies(t *testing.T) {
334 defer tests.PrepareTestEnv(t)()
335
336 owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
337 session := loginUser(t, owner.Name)
338 token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
339
340 repo, _, f := tests.CreateDeclarativeRepoWithOptions(t, owner, tests.DeclarativeRepoOptions{})
341 defer f()
342
343 createIssue := func(t *testing.T, title string) api.Issue {
344 t.Helper()
345
346 urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues", owner.Name, repo.Name)
347 req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateIssueOption{
348 Body: "",
349 Title: title,
350 }).AddTokenAuth(token)
351 resp := MakeRequest(t, req, http.StatusCreated)
352
353 var apiIssue api.Issue
354 DecodeJSON(t, resp, &apiIssue)
355
356 return apiIssue
357 }
358 addDependency := func(t *testing.T, issue, dependency api.Issue) {
359 t.Helper()
360
361 urlStr := fmt.Sprintf("/%s/%s/issues/%d/dependency/add", owner.Name, repo.Name, issue.Index)
362 req := NewRequestWithValues(t, "POST", urlStr, map[string]string{
363 "_csrf": GetCSRF(t, session, fmt.Sprintf("/%s/%s/issues/%d", owner.Name, repo.Name, issue.Index)),
364 "newDependency": fmt.Sprintf("%d", dependency.Index),
365 })
366 session.MakeRequest(t, req, http.StatusSeeOther)
367 }
368 removeDependency := func(t *testing.T, issue, dependency api.Issue) {
369 t.Helper()
370
371 urlStr := fmt.Sprintf("/%s/%s/issues/%d/dependency/delete", owner.Name, repo.Name, issue.Index)
372 req := NewRequestWithValues(t, "POST", urlStr, map[string]string{
373 "_csrf": GetCSRF(t, session, fmt.Sprintf("/%s/%s/issues/%d", owner.Name, repo.Name, issue.Index)),
374 "removeDependencyID": fmt.Sprintf("%d", dependency.Index),
375 "dependencyType": "blockedBy",
376 })
377 session.MakeRequest(t, req, http.StatusSeeOther)
378 }
379
380 assertHasDependency := func(t *testing.T, issueID, dependencyID int64, hasDependency bool) {
381 t.Helper()
382
383 urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/dependencies", owner.Name, repo.Name, issueID)
384 req := NewRequest(t, "GET", urlStr)
385 resp := MakeRequest(t, req, http.StatusOK)
386
387 var issues []api.Issue
388 DecodeJSON(t, resp, &issues)
389
390 if hasDependency {
391 assert.NotEmpty(t, issues)
392 assert.Equal(t, issues[0].Index, dependencyID)
393 } else {
394 assert.Empty(t, issues)
395 }
396 }
397
398 t.Run("Add dependency", func(t *testing.T) {
399 defer tests.PrintCurrentTest(t)()
400
401 issue1 := createIssue(t, "issue #1")
402 issue2 := createIssue(t, "issue #2")
403 addDependency(t, issue1, issue2)
404
405 assertHasDependency(t, issue1.Index, issue2.Index, true)
406 })
407
408 t.Run("Remove dependency", func(t *testing.T) {
409 defer tests.PrintCurrentTest(t)()
410
411 issue1 := createIssue(t, "issue #1")
412 issue2 := createIssue(t, "issue #2")
413 addDependency(t, issue1, issue2)
414 removeDependency(t, issue1, issue2)
415
416 assertHasDependency(t, issue1.Index, issue2.Index, false)
417 })
418}
419
420func TestEditIssue(t *testing.T) {
421 defer tests.PrepareTestEnv(t)()
422 session := loginUser(t, "user2")
423 issueURL := testNewIssue(t, session, "user2", "repo1", "Title", "Description")
424
425 req := NewRequestWithValues(t, "POST", fmt.Sprintf("%s/content", issueURL), map[string]string{
426 "_csrf": GetCSRF(t, session, issueURL),
427 "content": "modified content",
428 "context": fmt.Sprintf("/%s/%s", "user2", "repo1"),
429 })
430 session.MakeRequest(t, req, http.StatusOK)
431
432 req = NewRequestWithValues(t, "POST", fmt.Sprintf("%s/content", issueURL), map[string]string{
433 "_csrf": GetCSRF(t, session, issueURL),
434 "content": "modified content",
435 "context": fmt.Sprintf("/%s/%s", "user2", "repo1"),
436 })
437 session.MakeRequest(t, req, http.StatusBadRequest)
438
439 req = NewRequestWithValues(t, "POST", fmt.Sprintf("%s/content", issueURL), map[string]string{
440 "_csrf": GetCSRF(t, session, issueURL),
441 "content": "modified content",
442 "content_version": "1",
443 "context": fmt.Sprintf("/%s/%s", "user2", "repo1"),
444 })
445 session.MakeRequest(t, req, http.StatusOK)
446}
447
448func TestIssueCommentClose(t *testing.T) {
449 defer tests.PrepareTestEnv(t)()
450 session := loginUser(t, "user2")
451 issueURL := testNewIssue(t, session, "user2", "repo1", "Title", "Description")
452 testIssueAddComment(t, session, issueURL, "Test comment 1", "")
453 testIssueAddComment(t, session, issueURL, "Test comment 2", "")
454 testIssueAddComment(t, session, issueURL, "Test comment 3", "close")
455
456 // Validate that issue content has not been updated
457 req := NewRequest(t, "GET", issueURL)
458 resp := session.MakeRequest(t, req, http.StatusOK)
459 htmlDoc := NewHTMLParser(t, resp.Body)
460 val := htmlDoc.doc.Find(".comment-list .comment .render-content p").First().Text()
461 assert.Equal(t, "Description", val)
462}
463
464func TestIssueCommentDelete(t *testing.T) {
465 defer tests.PrepareTestEnv(t)()
466 session := loginUser(t, "user2")
467 issueURL := testNewIssue(t, session, "user2", "repo1", "Title", "Description")
468 comment1 := "Test comment 1"
469 commentID := testIssueAddComment(t, session, issueURL, comment1, "")
470 comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: commentID})
471 assert.Equal(t, comment1, comment.Content)
472
473 // Using the ID of a comment that does not belong to the repository must fail
474 req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d/delete", "user5", "repo4", commentID), map[string]string{
475 "_csrf": GetCSRF(t, session, issueURL),
476 })
477 session.MakeRequest(t, req, http.StatusNotFound)
478 req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d/delete", "user2", "repo1", commentID), map[string]string{
479 "_csrf": GetCSRF(t, session, issueURL),
480 })
481 session.MakeRequest(t, req, http.StatusOK)
482 unittest.AssertNotExistsBean(t, &issues_model.Comment{ID: commentID})
483}
484
485func TestIssueCommentAttachment(t *testing.T) {
486 defer tests.PrepareTestEnv(t)()
487 const repoURL = "user2/repo1"
488 const content = "Test comment 4"
489 const status = ""
490 session := loginUser(t, "user2")
491 issueURL := testNewIssue(t, session, "user2", "repo1", "Title", "Description")
492
493 req := NewRequest(t, "GET", issueURL)
494 resp := session.MakeRequest(t, req, http.StatusOK)
495
496 htmlDoc := NewHTMLParser(t, resp.Body)
497 link, exists := htmlDoc.doc.Find("#comment-form").Attr("action")
498 assert.True(t, exists, "The template has changed")
499
500 uuid := createAttachment(t, session, GetCSRF(t, session, repoURL), repoURL, "image.png", generateImg(), http.StatusOK)
501
502 commentCount := htmlDoc.doc.Find(".comment-list .comment .render-content").Length()
503
504 req = NewRequestWithValues(t, "POST", link, map[string]string{
505 "_csrf": htmlDoc.GetCSRF(),
506 "content": content,
507 "status": status,
508 "files": uuid,
509 })
510 resp = session.MakeRequest(t, req, http.StatusOK)
511
512 req = NewRequest(t, "GET", test.RedirectURL(resp))
513 resp = session.MakeRequest(t, req, http.StatusOK)
514
515 htmlDoc = NewHTMLParser(t, resp.Body)
516
517 val := htmlDoc.doc.Find(".comment-list .comment .render-content p").Eq(commentCount).Text()
518 assert.Equal(t, content, val)
519
520 idAttr, has := htmlDoc.doc.Find(".comment-list .comment").Eq(commentCount).Attr("id")
521 idStr := idAttr[strings.LastIndexByte(idAttr, '-')+1:]
522 assert.True(t, has)
523 id, err := strconv.Atoi(idStr)
524 require.NoError(t, err)
525 assert.NotEqual(t, 0, id)
526
527 req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/comments/%d/attachments", "user2", "repo1", id))
528 session.MakeRequest(t, req, http.StatusOK)
529
530 // Using the ID of a comment that does not belong to the repository must fail
531 req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/comments/%d/attachments", "user5", "repo4", id))
532 session.MakeRequest(t, req, http.StatusNotFound)
533}
534
535func TestIssueCommentUpdate(t *testing.T) {
536 defer tests.PrepareTestEnv(t)()
537 session := loginUser(t, "user2")
538 issueURL := testNewIssue(t, session, "user2", "repo1", "Title", "Description")
539 comment1 := "Test comment 1"
540 commentID := testIssueAddComment(t, session, issueURL, comment1, "")
541
542 comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: commentID})
543 assert.Equal(t, comment1, comment.Content)
544
545 modifiedContent := comment.Content + "MODIFIED"
546
547 // Using the ID of a comment that does not belong to the repository must fail
548 req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d", "user5", "repo4", commentID), map[string]string{
549 "_csrf": GetCSRF(t, session, issueURL),
550 "content": modifiedContent,
551 })
552 session.MakeRequest(t, req, http.StatusNotFound)
553
554 req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d", "user2", "repo1", commentID), map[string]string{
555 "_csrf": GetCSRF(t, session, issueURL),
556 "content": modifiedContent,
557 })
558 session.MakeRequest(t, req, http.StatusOK)
559
560 comment = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: commentID})
561 assert.Equal(t, modifiedContent, comment.Content)
562
563 // make the comment empty
564 req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d", "user2", "repo1", commentID), map[string]string{
565 "_csrf": GetCSRF(t, session, issueURL),
566 "content": "",
567 "content_version": fmt.Sprintf("%d", comment.ContentVersion),
568 })
569 session.MakeRequest(t, req, http.StatusOK)
570
571 comment = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: commentID})
572 assert.Empty(t, comment.Content)
573}
574
575func TestIssueCommentUpdateSimultaneously(t *testing.T) {
576 defer tests.PrepareTestEnv(t)()
577 session := loginUser(t, "user2")
578 issueURL := testNewIssue(t, session, "user2", "repo1", "Title", "Description")
579 comment1 := "Test comment 1"
580 commentID := testIssueAddComment(t, session, issueURL, comment1, "")
581
582 comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: commentID})
583 assert.Equal(t, comment1, comment.Content)
584
585 modifiedContent := comment.Content + "MODIFIED"
586
587 req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d", "user2", "repo1", commentID), map[string]string{
588 "_csrf": GetCSRF(t, session, issueURL),
589 "content": modifiedContent,
590 })
591 session.MakeRequest(t, req, http.StatusOK)
592
593 modifiedContent = comment.Content + "2"
594
595 req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d", "user2", "repo1", commentID), map[string]string{
596 "_csrf": GetCSRF(t, session, issueURL),
597 "content": modifiedContent,
598 })
599 session.MakeRequest(t, req, http.StatusBadRequest)
600
601 req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d", "user2", "repo1", commentID), map[string]string{
602 "_csrf": GetCSRF(t, session, issueURL),
603 "content": modifiedContent,
604 "content_version": "1",
605 })
606 session.MakeRequest(t, req, http.StatusOK)
607
608 comment = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: commentID})
609 assert.Equal(t, modifiedContent, comment.Content)
610 assert.Equal(t, 2, comment.ContentVersion)
611}
612
613func TestIssueReaction(t *testing.T) {
614 defer tests.PrepareTestEnv(t)()
615 session := loginUser(t, "user2")
616 issueURL := testNewIssue(t, session, "user2", "repo1", "Title", "Description")
617
618 req := NewRequest(t, "GET", issueURL)
619 resp := session.MakeRequest(t, req, http.StatusOK)
620 htmlDoc := NewHTMLParser(t, resp.Body)
621
622 req = NewRequestWithValues(t, "POST", path.Join(issueURL, "/reactions/react"), map[string]string{
623 "_csrf": htmlDoc.GetCSRF(),
624 "content": "8ball",
625 })
626 session.MakeRequest(t, req, http.StatusInternalServerError)
627 req = NewRequestWithValues(t, "POST", path.Join(issueURL, "/reactions/react"), map[string]string{
628 "_csrf": htmlDoc.GetCSRF(),
629 "content": "eyes",
630 })
631 session.MakeRequest(t, req, http.StatusOK)
632 req = NewRequestWithValues(t, "POST", path.Join(issueURL, "/reactions/unreact"), map[string]string{
633 "_csrf": htmlDoc.GetCSRF(),
634 "content": "eyes",
635 })
636 session.MakeRequest(t, req, http.StatusOK)
637}
638
639func TestIssueCrossReference(t *testing.T) {
640 defer tests.PrepareTestEnv(t)()
641
642 // Issue that will be referenced
643 _, issueBase := testIssueWithBean(t, "user2", 1, "Title", "Description")
644
645 // Ref from issue title
646 issueRefURL, issueRef := testIssueWithBean(t, "user2", 1, fmt.Sprintf("Title ref #%d", issueBase.Index), "Description")
647 unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{
648 IssueID: issueBase.ID,
649 RefRepoID: 1,
650 RefIssueID: issueRef.ID,
651 RefCommentID: 0,
652 RefIsPull: false,
653 RefAction: references.XRefActionNone,
654 })
655
656 // Edit title, neuter ref
657 testIssueChangeInfo(t, "user2", issueRefURL, "title", "Title no ref")
658 unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{
659 IssueID: issueBase.ID,
660 RefRepoID: 1,
661 RefIssueID: issueRef.ID,
662 RefCommentID: 0,
663 RefIsPull: false,
664 RefAction: references.XRefActionNeutered,
665 })
666
667 // Ref from issue content
668 issueRefURL, issueRef = testIssueWithBean(t, "user2", 1, "TitleXRef", fmt.Sprintf("Description ref #%d", issueBase.Index))
669 unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{
670 IssueID: issueBase.ID,
671 RefRepoID: 1,
672 RefIssueID: issueRef.ID,
673 RefCommentID: 0,
674 RefIsPull: false,
675 RefAction: references.XRefActionNone,
676 })
677
678 // Edit content, neuter ref
679 testIssueChangeInfo(t, "user2", issueRefURL, "content", "Description no ref")
680 unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{
681 IssueID: issueBase.ID,
682 RefRepoID: 1,
683 RefIssueID: issueRef.ID,
684 RefCommentID: 0,
685 RefIsPull: false,
686 RefAction: references.XRefActionNeutered,
687 })
688
689 // Ref from a comment
690 session := loginUser(t, "user2")
691 commentID := testIssueAddComment(t, session, issueRefURL, fmt.Sprintf("Adding ref from comment #%d", issueBase.Index), "")
692 comment := &issues_model.Comment{
693 IssueID: issueBase.ID,
694 RefRepoID: 1,
695 RefIssueID: issueRef.ID,
696 RefCommentID: commentID,
697 RefIsPull: false,
698 RefAction: references.XRefActionNone,
699 }
700 unittest.AssertExistsAndLoadBean(t, comment)
701
702 // Ref from a different repository
703 _, issueRef = testIssueWithBean(t, "user12", 10, "TitleXRef", fmt.Sprintf("Description ref user2/repo1#%d", issueBase.Index))
704 unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{
705 IssueID: issueBase.ID,
706 RefRepoID: 10,
707 RefIssueID: issueRef.ID,
708 RefCommentID: 0,
709 RefIsPull: false,
710 RefAction: references.XRefActionNone,
711 })
712}
713
714func testIssueWithBean(t *testing.T, user string, repoID int64, title, content string) (string, *issues_model.Issue) {
715 session := loginUser(t, user)
716 issueURL := testNewIssue(t, session, user, fmt.Sprintf("repo%d", repoID), title, content)
717 indexStr := issueURL[strings.LastIndexByte(issueURL, '/')+1:]
718 index, err := strconv.Atoi(indexStr)
719 require.NoError(t, err, "Invalid issue href: %s", issueURL)
720 issue := &issues_model.Issue{RepoID: repoID, Index: int64(index)}
721 unittest.AssertExistsAndLoadBean(t, issue)
722 return issueURL, issue
723}
724
725func testIssueChangeInfo(t *testing.T, user, issueURL, info, value string) {
726 session := loginUser(t, user)
727
728 req := NewRequest(t, "GET", issueURL)
729 resp := session.MakeRequest(t, req, http.StatusOK)
730 htmlDoc := NewHTMLParser(t, resp.Body)
731
732 req = NewRequestWithValues(t, "POST", path.Join(issueURL, info), map[string]string{
733 "_csrf": htmlDoc.GetCSRF(),
734 info: value,
735 })
736 _ = session.MakeRequest(t, req, http.StatusOK)
737}
738
739func TestIssueRedirect(t *testing.T) {
740 defer tests.PrepareTestEnv(t)()
741 session := loginUser(t, "user2")
742
743 // Test external tracker where style not set (shall default numeric)
744 req := NewRequest(t, "GET", path.Join("org26", "repo_external_tracker", "issues", "1"))
745 resp := session.MakeRequest(t, req, http.StatusSeeOther)
746 assert.Equal(t, "https://tracker.com/org26/repo_external_tracker/issues/1", test.RedirectURL(resp))
747
748 // Test external tracker with numeric style
749 req = NewRequest(t, "GET", path.Join("org26", "repo_external_tracker_numeric", "issues", "1"))
750 resp = session.MakeRequest(t, req, http.StatusSeeOther)
751 assert.Equal(t, "https://tracker.com/org26/repo_external_tracker_numeric/issues/1", test.RedirectURL(resp))
752
753 // Test external tracker with alphanumeric style (for a pull request)
754 req = NewRequest(t, "GET", path.Join("org26", "repo_external_tracker_alpha", "issues", "1"))
755 resp = session.MakeRequest(t, req, http.StatusSeeOther)
756 assert.Equal(t, "/"+path.Join("org26", "repo_external_tracker_alpha", "pulls", "1"), test.RedirectURL(resp))
757}
758
759func TestSearchIssues(t *testing.T) {
760 defer tests.PrepareTestEnv(t)()
761
762 session := loginUser(t, "user2")
763
764 expectedIssueCount := 20 // from the fixtures
765 if expectedIssueCount > setting.UI.IssuePagingNum {
766 expectedIssueCount = setting.UI.IssuePagingNum
767 }
768
769 link, _ := url.Parse("/issues/search")
770 req := NewRequest(t, "GET", link.String())
771 resp := session.MakeRequest(t, req, http.StatusOK)
772 var apiIssues []*api.Issue
773 DecodeJSON(t, resp, &apiIssues)
774 assert.Len(t, apiIssues, expectedIssueCount)
775
776 since := "2000-01-01T00:50:01+00:00" // 946687801
777 before := time.Unix(999307200, 0).Format(time.RFC3339)
778 query := url.Values{}
779 query.Add("since", since)
780 query.Add("before", before)
781 link.RawQuery = query.Encode()
782 req = NewRequest(t, "GET", link.String())
783 resp = session.MakeRequest(t, req, http.StatusOK)
784 DecodeJSON(t, resp, &apiIssues)
785 assert.Len(t, apiIssues, 11)
786 query.Del("since")
787 query.Del("before")
788
789 query.Add("state", "closed")
790 link.RawQuery = query.Encode()
791 req = NewRequest(t, "GET", link.String())
792 resp = session.MakeRequest(t, req, http.StatusOK)
793 DecodeJSON(t, resp, &apiIssues)
794 assert.Len(t, apiIssues, 2)
795
796 query.Set("state", "all")
797 link.RawQuery = query.Encode()
798 req = NewRequest(t, "GET", link.String())
799 resp = session.MakeRequest(t, req, http.StatusOK)
800 DecodeJSON(t, resp, &apiIssues)
801 assert.Equal(t, "22", resp.Header().Get("X-Total-Count"))
802 assert.Len(t, apiIssues, 20)
803
804 query.Add("limit", "5")
805 link.RawQuery = query.Encode()
806 req = NewRequest(t, "GET", link.String())
807 resp = session.MakeRequest(t, req, http.StatusOK)
808 DecodeJSON(t, resp, &apiIssues)
809 assert.Equal(t, "22", resp.Header().Get("X-Total-Count"))
810 assert.Len(t, apiIssues, 5)
811
812 query = url.Values{"assigned": {"true"}, "state": {"all"}}
813 link.RawQuery = query.Encode()
814 req = NewRequest(t, "GET", link.String())
815 resp = session.MakeRequest(t, req, http.StatusOK)
816 DecodeJSON(t, resp, &apiIssues)
817 assert.Len(t, apiIssues, 2)
818
819 query = url.Values{"milestones": {"milestone1"}, "state": {"all"}}
820 link.RawQuery = query.Encode()
821 req = NewRequest(t, "GET", link.String())
822 resp = session.MakeRequest(t, req, http.StatusOK)
823 DecodeJSON(t, resp, &apiIssues)
824 assert.Len(t, apiIssues, 1)
825
826 query = url.Values{"milestones": {"milestone1,milestone3"}, "state": {"all"}}
827 link.RawQuery = query.Encode()
828 req = NewRequest(t, "GET", link.String())
829 resp = session.MakeRequest(t, req, http.StatusOK)
830 DecodeJSON(t, resp, &apiIssues)
831 assert.Len(t, apiIssues, 2)
832
833 query = url.Values{"owner": {"user2"}} // user
834 link.RawQuery = query.Encode()
835 req = NewRequest(t, "GET", link.String())
836 resp = session.MakeRequest(t, req, http.StatusOK)
837 DecodeJSON(t, resp, &apiIssues)
838 assert.Len(t, apiIssues, 8)
839
840 query = url.Values{"owner": {"org3"}} // organization
841 link.RawQuery = query.Encode()
842 req = NewRequest(t, "GET", link.String())
843 resp = session.MakeRequest(t, req, http.StatusOK)
844 DecodeJSON(t, resp, &apiIssues)
845 assert.Len(t, apiIssues, 5)
846
847 query = url.Values{"owner": {"org3"}, "team": {"team1"}} // organization + team
848 link.RawQuery = query.Encode()
849 req = NewRequest(t, "GET", link.String())
850 resp = session.MakeRequest(t, req, http.StatusOK)
851 DecodeJSON(t, resp, &apiIssues)
852 assert.Len(t, apiIssues, 2)
853}
854
855func TestSearchIssuesWithLabels(t *testing.T) {
856 defer tests.PrepareTestEnv(t)()
857
858 expectedIssueCount := 20 // from the fixtures
859 if expectedIssueCount > setting.UI.IssuePagingNum {
860 expectedIssueCount = setting.UI.IssuePagingNum
861 }
862
863 session := loginUser(t, "user1")
864 link, _ := url.Parse("/issues/search")
865 query := url.Values{}
866 var apiIssues []*api.Issue
867
868 link.RawQuery = query.Encode()
869 req := NewRequest(t, "GET", link.String())
870 resp := session.MakeRequest(t, req, http.StatusOK)
871 DecodeJSON(t, resp, &apiIssues)
872 assert.Len(t, apiIssues, expectedIssueCount)
873
874 query.Add("labels", "label1")
875 link.RawQuery = query.Encode()
876 req = NewRequest(t, "GET", link.String())
877 resp = session.MakeRequest(t, req, http.StatusOK)
878 DecodeJSON(t, resp, &apiIssues)
879 assert.Len(t, apiIssues, 2)
880
881 // multiple labels
882 query.Set("labels", "label1,label2")
883 link.RawQuery = query.Encode()
884 req = NewRequest(t, "GET", link.String())
885 resp = session.MakeRequest(t, req, http.StatusOK)
886 DecodeJSON(t, resp, &apiIssues)
887 assert.Len(t, apiIssues, 2)
888
889 // an org label
890 query.Set("labels", "orglabel4")
891 link.RawQuery = query.Encode()
892 req = NewRequest(t, "GET", link.String())
893 resp = session.MakeRequest(t, req, http.StatusOK)
894 DecodeJSON(t, resp, &apiIssues)
895 assert.Len(t, apiIssues, 1)
896
897 // org and repo label
898 query.Set("labels", "label2,orglabel4")
899 query.Add("state", "all")
900 link.RawQuery = query.Encode()
901 req = NewRequest(t, "GET", link.String())
902 resp = session.MakeRequest(t, req, http.StatusOK)
903 DecodeJSON(t, resp, &apiIssues)
904 assert.Len(t, apiIssues, 2)
905
906 // org and repo label which share the same issue
907 query.Set("labels", "label1,orglabel4")
908 link.RawQuery = query.Encode()
909 req = NewRequest(t, "GET", link.String())
910 resp = session.MakeRequest(t, req, http.StatusOK)
911 DecodeJSON(t, resp, &apiIssues)
912 assert.Len(t, apiIssues, 2)
913}
914
915func TestGetIssueInfo(t *testing.T) {
916 defer tests.PrepareTestEnv(t)()
917
918 issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 10})
919 repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
920 owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
921 require.NoError(t, issue.LoadAttributes(db.DefaultContext))
922 assert.Equal(t, int64(1019307200), int64(issue.DeadlineUnix))
923 assert.Equal(t, api.StateOpen, issue.State())
924
925 session := loginUser(t, owner.Name)
926
927 urlStr := fmt.Sprintf("/%s/%s/issues/%d/info", owner.Name, repo.Name, issue.Index)
928 req := NewRequest(t, "GET", urlStr)
929 resp := session.MakeRequest(t, req, http.StatusOK)
930 var apiIssue api.Issue
931 DecodeJSON(t, resp, &apiIssue)
932
933 assert.Equal(t, issue.ID, apiIssue.ID)
934}
935
936func TestIssuePinMove(t *testing.T) {
937 defer tests.PrepareTestEnv(t)()
938 session := loginUser(t, "user2")
939 issueURL, issue := testIssueWithBean(t, "user2", 1, "Title", "Content")
940 assert.Equal(t, 0, issue.PinOrder)
941
942 req := NewRequestWithValues(t, "POST", fmt.Sprintf("%s/pin", issueURL), map[string]string{
943 "_csrf": GetCSRF(t, session, issueURL),
944 })
945 session.MakeRequest(t, req, http.StatusOK)
946 issue = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issue.ID})
947
948 position := 1
949 assert.Equal(t, position, issue.PinOrder)
950
951 newPosition := 2
952
953 // Using the ID of an issue that does not belong to the repository must fail
954 {
955 session5 := loginUser(t, "user5")
956 movePinURL := "/user5/repo4/issues/move_pin?_csrf=" + GetCSRF(t, session5, issueURL)
957 req = NewRequestWithJSON(t, "POST", movePinURL, map[string]any{
958 "id": issue.ID,
959 "position": newPosition,
960 })
961 session5.MakeRequest(t, req, http.StatusNotFound)
962
963 issue = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issue.ID})
964 assert.Equal(t, position, issue.PinOrder)
965 }
966
967 movePinURL := issueURL[:strings.LastIndexByte(issueURL, '/')] + "/move_pin?_csrf=" + GetCSRF(t, session, issueURL)
968 req = NewRequestWithJSON(t, "POST", movePinURL, map[string]any{
969 "id": issue.ID,
970 "position": newPosition,
971 })
972 session.MakeRequest(t, req, http.StatusNoContent)
973
974 issue = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issue.ID})
975 assert.Equal(t, newPosition, issue.PinOrder)
976}
977
978func TestUpdateIssueDeadline(t *testing.T) {
979 defer tests.PrepareTestEnv(t)()
980
981 issueBefore := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 10})
982 repoBefore := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issueBefore.RepoID})
983 owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repoBefore.OwnerID})
984 require.NoError(t, issueBefore.LoadAttributes(db.DefaultContext))
985 assert.Equal(t, int64(1019307200), int64(issueBefore.DeadlineUnix))
986 assert.Equal(t, api.StateOpen, issueBefore.State())
987
988 session := loginUser(t, owner.Name)
989
990 issueURL := fmt.Sprintf("%s/%s/issues/%d", owner.Name, repoBefore.Name, issueBefore.Index)
991 req := NewRequest(t, "GET", issueURL)
992 resp := session.MakeRequest(t, req, http.StatusOK)
993 htmlDoc := NewHTMLParser(t, resp.Body)
994
995 urlStr := issueURL + "/deadline?_csrf=" + htmlDoc.GetCSRF()
996 req = NewRequestWithJSON(t, "POST", urlStr, map[string]string{
997 "due_date": "2022-04-06T00:00:00.000Z",
998 })
999
1000 resp = session.MakeRequest(t, req, http.StatusCreated)
1001 var apiIssue api.IssueDeadline
1002 DecodeJSON(t, resp, &apiIssue)
1003
1004 assert.Equal(t, "2022-04-06", apiIssue.Deadline.Format("2006-01-02"))
1005}
1006
1007func TestUpdateIssueTitle(t *testing.T) {
1008 defer tests.PrepareTestEnv(t)()
1009
1010 issueBefore := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
1011 repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issueBefore.RepoID})
1012 owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
1013
1014 require.NoError(t, issueBefore.LoadAttributes(db.DefaultContext))
1015 assert.Equal(t, "issue1", issueBefore.Title)
1016
1017 issueTitleUpdateTests := []struct {
1018 title string
1019 expectedHTTPCode int
1020 }{
1021 {
1022 title: "normal-title",
1023 expectedHTTPCode: http.StatusOK,
1024 },
1025 {
1026 title: "extra-long-title-with-exactly-255-chars-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
1027 expectedHTTPCode: http.StatusOK,
1028 },
1029 {
1030 title: "",
1031 expectedHTTPCode: http.StatusBadRequest,
1032 },
1033 {
1034 title: " ",
1035 expectedHTTPCode: http.StatusBadRequest,
1036 },
1037 {
1038 title: "extra-long-title-over-255-chars-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
1039 expectedHTTPCode: http.StatusBadRequest,
1040 },
1041 }
1042
1043 session := loginUser(t, owner.Name)
1044 issueURL := fmt.Sprintf("%s/%s/issues/%d", owner.Name, repo.Name, issueBefore.Index)
1045 urlStr := issueURL + "/title"
1046
1047 for _, issueTitleUpdateTest := range issueTitleUpdateTests {
1048 req := NewRequestWithValues(t, "POST", urlStr, map[string]string{
1049 "title": issueTitleUpdateTest.title,
1050 "_csrf": GetCSRF(t, session, issueURL),
1051 })
1052
1053 resp := session.MakeRequest(t, req, issueTitleUpdateTest.expectedHTTPCode)
1054
1055 // JSON data is received only if the request succeeds
1056 if issueTitleUpdateTest.expectedHTTPCode == http.StatusOK {
1057 issueAfter := struct {
1058 Title string `json:"title"`
1059 }{}
1060
1061 DecodeJSON(t, resp, &issueAfter)
1062 assert.Equal(t, issueTitleUpdateTest.title, issueAfter.Title)
1063 }
1064 }
1065}
1066
1067func TestIssueReferenceURL(t *testing.T) {
1068 defer tests.PrepareTestEnv(t)()
1069 session := loginUser(t, "user2")
1070
1071 issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
1072 repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
1073
1074 req := NewRequest(t, "GET", fmt.Sprintf("%s/issues/%d", repo.FullName(), issue.Index))
1075 resp := session.MakeRequest(t, req, http.StatusOK)
1076 htmlDoc := NewHTMLParser(t, resp.Body)
1077
1078 // the "reference" uses relative URLs, then JS code will convert them to absolute URLs for current origin, in case users are using multiple domains
1079 ref, _ := htmlDoc.Find(`.timeline-item.comment.first .reference-issue`).Attr("data-reference")
1080 assert.Equal(t, "/user2/repo1/issues/1#issue-1", ref)
1081
1082 ref, _ = htmlDoc.Find(`.timeline-item.comment:not(.first) .reference-issue`).Attr("data-reference")
1083 assert.Equal(t, "/user2/repo1/issues/1#issuecomment-2", ref)
1084}
1085
1086func TestGetContentHistory(t *testing.T) {
1087 defer tests.AddFixtures("tests/integration/fixtures/TestGetContentHistory/")()
1088 defer tests.PrepareTestEnv(t)()
1089
1090 issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
1091 repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
1092 issueURL := fmt.Sprintf("%s/issues/%d", repo.FullName(), issue.Index)
1093 contentHistory := unittest.AssertExistsAndLoadBean(t, &issues_model.ContentHistory{ID: 2, IssueID: issue.ID})
1094 contentHistoryURL := fmt.Sprintf("%s/issues/%d/content-history/detail?comment_id=%d&history_id=%d", repo.FullName(), issue.Index, contentHistory.CommentID, contentHistory.ID)
1095
1096 type contentHistoryResp struct {
1097 CanSoftDelete bool `json:"canSoftDelete"`
1098 HistoryID int `json:"historyId"`
1099 PrevHistoryID int `json:"prevHistoryId"`
1100 }
1101
1102 testCase := func(t *testing.T, session *TestSession, canDelete bool) {
1103 t.Helper()
1104 contentHistoryURL := contentHistoryURL + "&_csrf=" + GetCSRF(t, session, issueURL)
1105
1106 req := NewRequest(t, "GET", contentHistoryURL)
1107 resp := session.MakeRequest(t, req, http.StatusOK)
1108
1109 var respJSON contentHistoryResp
1110 DecodeJSON(t, resp, &respJSON)
1111
1112 assert.Equal(t, canDelete, respJSON.CanSoftDelete)
1113 assert.EqualValues(t, contentHistory.ID, respJSON.HistoryID)
1114 assert.EqualValues(t, contentHistory.ID-1, respJSON.PrevHistoryID)
1115 }
1116
1117 t.Run("Anonymous", func(t *testing.T) {
1118 defer tests.PrintCurrentTest(t)()
1119 testCase(t, emptyTestSession(t), false)
1120 })
1121
1122 t.Run("Another user", func(t *testing.T) {
1123 defer tests.PrintCurrentTest(t)()
1124 testCase(t, loginUser(t, "user8"), false)
1125 })
1126
1127 t.Run("Repo owner", func(t *testing.T) {
1128 defer tests.PrintCurrentTest(t)()
1129 testCase(t, loginUser(t, "user2"), true)
1130 })
1131
1132 t.Run("Poster", func(t *testing.T) {
1133 defer tests.PrintCurrentTest(t)()
1134 testCase(t, loginUser(t, "user5"), true)
1135 })
1136}
1137
1138func TestCommitRefComment(t *testing.T) {
1139 defer tests.AddFixtures("tests/integration/fixtures/TestCommitRefComment/")()
1140 defer tests.PrepareTestEnv(t)()
1141
1142 t.Run("Pull request", func(t *testing.T) {
1143 defer tests.PrintCurrentTest(t)()
1144
1145 req := NewRequest(t, "GET", "/user2/repo1/pulls/2")
1146 resp := MakeRequest(t, req, http.StatusOK)
1147 htmlDoc := NewHTMLParser(t, resp.Body)
1148
1149 event := htmlDoc.Find("#issuecomment-1000 .text").Text()
1150 assert.Contains(t, event, "referenced this pull request")
1151 })
1152
1153 t.Run("Issue", func(t *testing.T) {
1154 defer tests.PrintCurrentTest(t)()
1155
1156 req := NewRequest(t, "GET", "/user2/repo1/issues/1")
1157 resp := MakeRequest(t, req, http.StatusOK)
1158 htmlDoc := NewHTMLParser(t, resp.Body)
1159
1160 event := htmlDoc.Find("#issuecomment-1001 .text").Text()
1161 assert.Contains(t, event, "referenced this issue")
1162 })
1163}
1164
1165func TestIssueFilterNoFollow(t *testing.T) {
1166 defer tests.PrepareTestEnv(t)()
1167
1168 // Check that every link in the filter list has rel="nofollow".
1169 t.Run("Issue lists", func(t *testing.T) {
1170 req := NewRequest(t, "GET", "/user2/repo1/issues")
1171 resp := MakeRequest(t, req, http.StatusOK)
1172 htmlDoc := NewHTMLParser(t, resp.Body)
1173
1174 filterLinks := htmlDoc.Find(".issue-list-toolbar-right a[href*=\"?q=\"], .labels-list a[href*=\"?q=\"]")
1175 assert.Positive(t, filterLinks.Length())
1176 filterLinks.Each(func(i int, link *goquery.Selection) {
1177 rel, has := link.Attr("rel")
1178 assert.True(t, has)
1179 assert.Equal(t, "nofollow", rel)
1180 })
1181 })
1182
1183 t.Run("Issue page", func(t *testing.T) {
1184 req := NewRequest(t, "GET", "/user2/repo1/issues/1")
1185 resp := MakeRequest(t, req, http.StatusOK)
1186 htmlDoc := NewHTMLParser(t, resp.Body)
1187
1188 filterLinks := htmlDoc.Find(".timeline .labels-list a[href*=\"?labels=\"], .issue-content-right .labels-list a[href*=\"?labels=\"]")
1189 assert.Positive(t, filterLinks.Length())
1190 filterLinks.Each(func(i int, link *goquery.Selection) {
1191 rel, has := link.Attr("rel")
1192 assert.True(t, has)
1193 assert.Equal(t, "nofollow", rel)
1194 })
1195 })
1196}
1197
1198func TestIssueForm(t *testing.T) {
1199 onGiteaRun(t, func(t *testing.T, u *url.URL) {
1200 user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
1201 session := loginUser(t, user2.Name)
1202 repo, _, f := tests.CreateDeclarativeRepo(t, user2, "",
1203 []unit_model.Type{unit_model.TypeCode, unit_model.TypeIssues}, nil,
1204 []*files_service.ChangeRepoFile{
1205 {
1206 Operation: "create",
1207 TreePath: ".forgejo/issue_template/test.yaml",
1208 ContentReader: strings.NewReader(`name: Test
1209about: Hello World
1210body:
1211 - type: checkboxes
1212 id: test
1213 attributes:
1214 label: Test
1215 options:
1216 - label: This is a label
1217`),
1218 },
1219 },
1220 )
1221 defer f()
1222
1223 t.Run("Choose list", func(t *testing.T) {
1224 defer tests.PrintCurrentTest(t)()
1225
1226 req := NewRequest(t, "GET", repo.Link()+"/issues/new/choose")
1227 resp := session.MakeRequest(t, req, http.StatusOK)
1228 htmlDoc := NewHTMLParser(t, resp.Body)
1229
1230 htmlDoc.AssertElement(t, "a[href$='/issues/new?template=.forgejo%2fissue_template%2ftest.yaml']", true)
1231 })
1232
1233 t.Run("Issue template", func(t *testing.T) {
1234 defer tests.PrintCurrentTest(t)()
1235
1236 req := NewRequest(t, "GET", repo.Link()+"/issues/new?template=.forgejo%2fissue_template%2ftest.yaml")
1237 resp := session.MakeRequest(t, req, http.StatusOK)
1238 htmlDoc := NewHTMLParser(t, resp.Body)
1239
1240 htmlDoc.AssertElement(t, "#new-issue .field .ui.checkbox input[name='form-field-test-0']", true)
1241 checkboxLabel := htmlDoc.Find("#new-issue .field .ui.checkbox label").Text()
1242 assert.Contains(t, checkboxLabel, "This is a label")
1243 })
1244 })
1245}
1246
1247func TestIssueUnsubscription(t *testing.T) {
1248 onGiteaRun(t, func(t *testing.T, u *url.URL) {
1249 defer tests.PrepareTestEnv(t)()
1250
1251 user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
1252 repo, _, f := tests.CreateDeclarativeRepoWithOptions(t, user, tests.DeclarativeRepoOptions{
1253 AutoInit: optional.Some(false),
1254 })
1255 defer f()
1256 session := loginUser(t, user.Name)
1257
1258 issueURL := testNewIssue(t, session, user.Name, repo.Name, "Issue title", "Description")
1259 req := NewRequestWithValues(t, "POST", fmt.Sprintf("%s/watch", issueURL), map[string]string{
1260 "_csrf": GetCSRF(t, session, issueURL),
1261 "watch": "0",
1262 })
1263 session.MakeRequest(t, req, http.StatusOK)
1264 })
1265}
1266
1267func TestIssueLabelList(t *testing.T) {
1268 defer tests.PrepareTestEnv(t)()
1269 // The label list should always be present. When no labels are selected, .no-select is visible, otherwise hidden.
1270 labelListSelector := ".labels.list .labels-list"
1271 hiddenClass := "tw-hidden"
1272
1273 t.Run("Test label list", func(t *testing.T) {
1274 defer tests.PrintCurrentTest(t)()
1275
1276 req := NewRequest(t, "GET", "/user2/repo1/issues/1")
1277 resp := MakeRequest(t, req, http.StatusOK)
1278 htmlDoc := NewHTMLParser(t, resp.Body)
1279
1280 htmlDoc.AssertElement(t, labelListSelector, true)
1281 htmlDoc.AssertElement(t, ".labels.list .no-select."+hiddenClass, true)
1282 })
1283}
1284
1285func TestIssueUserDashboard(t *testing.T) {
1286 defer tests.PrepareTestEnv(t)()
1287
1288 user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
1289 session := loginUser(t, user.Name)
1290
1291 // assert 'created_by' is the default filter
1292 const sel = ".dashboard .ui.list-header.dropdown .ui.menu a.active.item[href^='?type=created_by']"
1293
1294 for _, path := range []string{"/issues", "/pulls"} {
1295 req := NewRequest(t, "GET", path)
1296 resp := session.MakeRequest(t, req, http.StatusOK)
1297 htmlDoc := NewHTMLParser(t, resp.Body)
1298 htmlDoc.AssertElement(t, sel, true)
1299 }
1300}
1301
1302func TestIssueOrgDashboard(t *testing.T) {
1303 defer tests.PrepareTestEnv(t)()
1304
1305 user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
1306 session := loginUser(t, user.Name)
1307
1308 // assert 'your_repositories' is the default filter for org dashboards
1309 const sel = ".dashboard .ui.list-header.dropdown .ui.menu a.active.item[href^='?type=your_repositories']"
1310
1311 for _, path := range []string{"/org/org3/issues", "/org/org3/pulls"} {
1312 req := NewRequest(t, "GET", path)
1313 resp := session.MakeRequest(t, req, http.StatusOK)
1314 htmlDoc := NewHTMLParser(t, resp.Body)
1315 htmlDoc.AssertElement(t, sel, true)
1316 }
1317}
1318
1319func TestIssueCount(t *testing.T) {
1320 defer tests.PrepareTestEnv(t)()
1321
1322 req := NewRequest(t, "GET", "/user2/repo1/issues")
1323 resp := MakeRequest(t, req, http.StatusOK)
1324
1325 htmlDoc := NewHTMLParser(t, resp.Body)
1326
1327 openCount := htmlDoc.doc.Find("a[data-test-name='open-issue-count']").Text()
1328 assert.Contains(t, openCount, "1\u00a0Open")
1329
1330 closedCount := htmlDoc.doc.Find("a[data-test-name='closed-issue-count']").Text()
1331 assert.Contains(t, closedCount, "1\u00a0Closed")
1332
1333 allCount := htmlDoc.doc.Find("a[data-test-name='all-issue-count']").Text()
1334 assert.Contains(t, allCount, "2\u00a0All")
1335}
1336
1337func TestIssuePostersSearch(t *testing.T) {
1338 defer tests.PrepareTestEnv(t)()
1339
1340 type userSearchInfo struct {
1341 UserID int64 `json:"user_id"`
1342 UserName string `json:"username"`
1343 }
1344
1345 type userSearchResponse struct {
1346 Results []*userSearchInfo `json:"results"`
1347 }
1348
1349 t.Run("Name search", func(t *testing.T) {
1350 defer tests.PrintCurrentTest(t)()
1351 defer test.MockVariableValue(&setting.UI.DefaultShowFullName, false)()
1352
1353 req := NewRequest(t, "GET", "/user2/repo1/issues/posters?q=USer2")
1354 resp := MakeRequest(t, req, http.StatusOK)
1355
1356 var data userSearchResponse
1357 DecodeJSON(t, resp, &data)
1358
1359 assert.Len(t, data.Results, 1)
1360 assert.Equal(t, "user2", data.Results[0].UserName)
1361 assert.EqualValues(t, 2, data.Results[0].UserID)
1362 })
1363
1364 t.Run("Full name search", func(t *testing.T) {
1365 defer tests.PrintCurrentTest(t)()
1366 defer test.MockVariableValue(&setting.UI.DefaultShowFullName, true)()
1367
1368 req := NewRequest(t, "GET", "/user2/repo1/issues/posters?q=OnE")
1369 resp := MakeRequest(t, req, http.StatusOK)
1370
1371 var data userSearchResponse
1372 DecodeJSON(t, resp, &data)
1373
1374 assert.Len(t, data.Results, 1)
1375 assert.Equal(t, "user1", data.Results[0].UserName)
1376 assert.EqualValues(t, 1, data.Results[0].UserID)
1377 })
1378}