loading up the forgejo repo on tangled to test page performance
at forgejo 1378 lines 49 kB view raw
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}