// Copyright 2017 The Gitea Authors. All rights reserved.// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved.// SPDX-License-Identifier: MIT package integration import ( "fmt" "net/http" "net/http/httptest" "net/url" "path" "regexp" "strconv" "strings" "testing" "time" auth_model "forgejo.org/models/auth" "forgejo.org/models/db" issues_model "forgejo.org/models/issues" project_model "forgejo.org/models/project" repo_model "forgejo.org/models/repo" unit_model "forgejo.org/models/unit" "forgejo.org/models/unittest" user_model "forgejo.org/models/user" "forgejo.org/modules/indexer/issues" "forgejo.org/modules/optional" "forgejo.org/modules/references" "forgejo.org/modules/setting" api "forgejo.org/modules/structs" "forgejo.org/modules/test" "forgejo.org/modules/translation" files_service "forgejo.org/services/repository/files" user_service "forgejo.org/services/user" "forgejo.org/tests" "github.com/PuerkitoBio/goquery" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require") func getIssuesSelection(t testing.TB, htmlDoc *HTMLDoc) *goquery.Selection { issueList := htmlDoc.doc.Find("#issue-list") assert.Equal(t, 1, issueList.Length()) return issueList.Find(".flex-item").Find(".issue-title")} func getIssue(t *testing.T, repoID int64, issueSelection *goquery.Selection) *issues_model.Issue { href, exists := issueSelection.Attr("href") assert.True(t, exists) indexStr := href[strings.LastIndexByte(href, '/')+1:] index, err := strconv.Atoi(indexStr) require.NoError(t, err, "Invalid issue href: %s", href) return unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: repoID, Index: int64(index)})} func assertMatch(t testing.TB, issue *issues_model.Issue, keyword string) { matches := strings.Contains(strings.ToLower(issue.Title), keyword) || strings.Contains(strings.ToLower(issue.Content), keyword) for _, comment := range issue.Comments { matches = matches || strings.Contains( strings.ToLower(comment.Content), keyword, ) } assert.True(t, matches)} func TestNoLoginViewIssues(t *testing.T) { defer tests.PrepareTestEnv(t)() req := NewRequest(t, "GET", "/user2/repo1/issues") MakeRequest(t, req, http.StatusOK)} func TestViewIssues(t *testing.T) { defer tests.PrepareTestEnv(t)() req := NewRequest(t, "GET", "/user2/repo1/issues") resp := MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) search := htmlDoc.doc.Find(".list-header-search > .search > .input > input") placeholder, _ := search.Attr("placeholder") assert.Equal(t, "Search issues…", placeholder)} func TestViewIssuesSortByType(t *testing.T) { defer tests.PrepareTestEnv(t)() user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) session := loginUser(t, user.Name) req := NewRequest(t, "GET", repo.Link()+"/issues?type=created_by") resp := session.MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) issuesSelection := getIssuesSelection(t, htmlDoc) expectedNumIssues := unittest.GetCount(t, &issues_model.Issue{RepoID: repo.ID, PosterID: user.ID}, unittest.Cond("is_closed=?", false), unittest.Cond("is_pull=?", false), ) if expectedNumIssues > setting.UI.IssuePagingNum { expectedNumIssues = setting.UI.IssuePagingNum } assert.Equal(t, expectedNumIssues, issuesSelection.Length()) issuesSelection.Each(func(_ int, selection *goquery.Selection) { issue := getIssue(t, repo.ID, selection) assert.Equal(t, user.ID, issue.PosterID) })} func TestViewIssuesKeyword(t *testing.T) { defer tests.PrepareTestEnv(t)() repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ RepoID: repo.ID, Index: 1, }) issues.UpdateIssueIndexer(t.Context(), issue.ID) time.Sleep(time.Second * 1) const keyword = "first" req := NewRequestf(t, "GET", "%s/issues?q=%s", repo.Link(), keyword) resp := MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) issuesSelection := getIssuesSelection(t, htmlDoc) assert.Equal(t, 1, issuesSelection.Length()) issuesSelection.Each(func(_ int, selection *goquery.Selection) { issue := getIssue(t, repo.ID, selection) assert.False(t, issue.IsClosed) assert.False(t, issue.IsPull) assertMatch(t, issue, keyword) }) // keyword: 'firstt' // should not match when using phrase search req = NewRequestf(t, "GET", "%s/issues?q=\"%st\"", repo.Link(), keyword) resp = MakeRequest(t, req, http.StatusOK) htmlDoc = NewHTMLParser(t, resp.Body) issuesSelection = getIssuesSelection(t, htmlDoc) assert.Equal(t, 0, issuesSelection.Length()) // should match as 'first' when using a standard query req = NewRequestf(t, "GET", "%s/issues?q=%st", repo.Link(), keyword) resp = MakeRequest(t, req, http.StatusOK) htmlDoc = NewHTMLParser(t, resp.Body) issuesSelection = getIssuesSelection(t, htmlDoc) assert.Equal(t, 1, issuesSelection.Length()) issuesSelection.Each(func(_ int, selection *goquery.Selection) { issue := getIssue(t, repo.ID, selection) assert.False(t, issue.IsClosed) assert.False(t, issue.IsPull) assertMatch(t, issue, keyword) })} func TestViewIssuesSearchOptions(t *testing.T) { defer tests.PrepareTestEnv(t)() repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) // there are two issues in repo1, both bound to a project. Add one // that is not bound to any project. _, issueNoProject := testIssueWithBean(t, "user2", 1, "Title", "Description") t.Run("All issues", func(t *testing.T) { req := NewRequestf(t, "GET", "%s/issues?state=all", repo.Link()) resp := MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) issuesSelection := getIssuesSelection(t, htmlDoc) assert.Equal(t, 3, issuesSelection.Length()) }) t.Run("Issues with no project", func(t *testing.T) { req := NewRequestf(t, "GET", "%s/issues?state=all&project=-1", repo.Link()) resp := MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) issuesSelection := getIssuesSelection(t, htmlDoc) assert.Equal(t, 1, issuesSelection.Length()) issuesSelection.Each(func(_ int, selection *goquery.Selection) { issue := getIssue(t, repo.ID, selection) assert.Equal(t, issueNoProject.ID, issue.ID) }) }) t.Run("Issues with a specific project", func(t *testing.T) { project := unittest.AssertExistsAndLoadBean(t, &project_model.Project{ID: 1}) req := NewRequestf(t, "GET", "%s/issues?state=all&project=%d", repo.Link(), project.ID) resp := MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) issuesSelection := getIssuesSelection(t, htmlDoc) assert.Equal(t, 2, issuesSelection.Length()) found := map[int64]bool{ 1: false, 5: false, } issuesSelection.Each(func(_ int, selection *goquery.Selection) { issue := getIssue(t, repo.ID, selection) found[issue.ID] = true }) assert.Len(t, found, 2) assert.True(t, found[1]) assert.True(t, found[5]) })} func TestNoLoginViewIssue(t *testing.T) { defer tests.PrepareTestEnv(t)() req := NewRequest(t, "GET", "/user2/repo1/issues/1") MakeRequest(t, req, http.StatusOK)} func TestViewIssueCommentBox(t *testing.T) { defer tests.PrepareTestEnv(t)() locale := translation.NewLocale("en-US") repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) collaborator := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) viewer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) ownerSession := loginUser(t, owner.Name) collaboratorSession := loginUser(t, collaborator.Name) viewerSession := loginUser(t, viewer.Name) openIssueURL, _ := testIssueWithBean(t, owner.Name, repo.ID, "Open", "Description") warningSignin := string(locale.Tr("repo.issues.sign_in_require_desc", fmt.Sprintf("/user/login?redirect_to=%s", url.QueryEscape(openIssueURL)))) warningLocked := locale.TrString("discussion.locked") warningBlocked := locale.TrString("repo.comment.blocked_by_user") assertCommentForm := func(session *TestSession, url string, shouldExist bool) *HTMLDoc { var resp *httptest.ResponseRecorder req := NewRequest(t, "GET", url) if session != nil { resp = session.MakeRequest(t, req, http.StatusOK) } else { resp = MakeRequest(t, req, http.StatusOK) } htmlDoc := NewHTMLParser(t, resp.Body) htmlDoc.AssertElement(t, ".issue-content #comment-form", shouldExist) return htmlDoc } assertWarning := func(session *TestSession, url, warning string) { htmlDoc := assertCommentForm(session, url, false) timeline := htmlDoc.doc.Find(".issue-content .ui.timeline").Children() assert.Positivef(t, timeline.Length(), "There should be at least one entry in the timeline") html, _ := timeline.Last().Html() assert.Equal(t, strings.TrimSpace(html), warning) } t.Run("Owner can see comment box", func(t *testing.T) { assertCommentForm(ownerSession, openIssueURL, true) }) t.Run("Collaborator can see comment box", func(t *testing.T) { assertCommentForm(collaboratorSession, openIssueURL, true) }) t.Run("Viewer can see comment box", func(t *testing.T) { assertCommentForm(viewerSession, openIssueURL, true) }) t.Run("Anonymous will see singin warning", func(t *testing.T) { assertWarning(nil, openIssueURL, warningSignin) }) lockedIssueURL, lockedIssue := testIssueWithBean(t, owner.Name, repo.ID, "Locked", "Description") // Lock the issue req := NewRequest(t, "GET", lockedIssueURL) resp := ownerSession.MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) req = NewRequestWithValues(t, "POST", fmt.Sprintf("%s/lock", lockedIssueURL), map[string]string{ "_csrf": htmlDoc.GetCSRF(), "reason": "Too heated", }) _ = ownerSession.MakeRequest(t, req, http.StatusOK) lockedIssue = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: lockedIssue.ID}) assert.True(t, lockedIssue.IsLocked) t.Run("Owner can see comment box in locked issue", func(t *testing.T) { assertCommentForm(ownerSession, lockedIssueURL, true) }) t.Run("Collaborator can see comment box in locked issue", func(t *testing.T) { assertCommentForm(collaboratorSession, lockedIssueURL, true) }) t.Run("Viewer will see warning in locked issue", func(t *testing.T) { assertWarning(viewerSession, lockedIssueURL, warningLocked) }) t.Run("Anonymous will see warning in locked issue", func(t *testing.T) { assertWarning(nil, lockedIssueURL, warningLocked) }) err := user_service.BlockUser(db.DefaultContext, owner.ID, viewer.ID) require.NoError(t, err) t.Run("Blocked viewer will see warning", func(t *testing.T) { assertWarning(viewerSession, lockedIssueURL, warningBlocked) assertWarning(viewerSession, openIssueURL, warningBlocked) })} func testNewIssue(t *testing.T, session *TestSession, user, repo, title, content string) string { req := NewRequest(t, "GET", path.Join(user, repo, "issues", "new")) resp := session.MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) link, exists := htmlDoc.doc.Find("form.ui.form").Attr("action") assert.True(t, exists, "The template has changed") req = NewRequestWithValues(t, "POST", link, map[string]string{ "_csrf": htmlDoc.GetCSRF(), "title": title, "content": content, }) resp = session.MakeRequest(t, req, http.StatusOK) issueURL := test.RedirectURL(resp) req = NewRequest(t, "GET", issueURL) resp = session.MakeRequest(t, req, http.StatusOK) htmlDoc = NewHTMLParser(t, resp.Body) val := htmlDoc.doc.Find("#issue-title-display").Text() assert.Contains(t, val, title) // test for first line only and if it contains only letters and spaces contentFirstLine := strings.Split(content, "\n")[0] patNotLetterOrSpace := regexp.MustCompile(`[^\p{L}\s]`) if len(contentFirstLine) != 0 && !patNotLetterOrSpace.MatchString(contentFirstLine) { val = htmlDoc.doc.Find(".comment .render-content p").First().Text() assert.Equal(t, contentFirstLine, val) } return issueURL} func testIssueAddComment(t *testing.T, session *TestSession, issueURL, content, status string) int64 { req := NewRequest(t, "GET", issueURL) resp := session.MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) link, exists := htmlDoc.doc.Find("#comment-form").Attr("action") assert.True(t, exists, "The template has changed") commentCount := htmlDoc.doc.Find(".comment-list .comment .render-content").Length() req = NewRequestWithValues(t, "POST", link, map[string]string{ "_csrf": htmlDoc.GetCSRF(), "content": content, "status": status, }) resp = session.MakeRequest(t, req, http.StatusOK) req = NewRequest(t, "GET", test.RedirectURL(resp)) resp = session.MakeRequest(t, req, http.StatusOK) htmlDoc = NewHTMLParser(t, resp.Body) val := htmlDoc.doc.Find(".comment-list .comment .render-content p").Eq(commentCount).Text() assert.Equal(t, content, val) idAttr, has := htmlDoc.doc.Find(".comment-list .comment").Eq(commentCount).Attr("id") idStr := idAttr[strings.LastIndexByte(idAttr, '-')+1:] assert.True(t, has) id, err := strconv.Atoi(idStr) require.NoError(t, err) return int64(id)} func TestNewIssue(t *testing.T) { defer tests.PrepareTestEnv(t)() session := loginUser(t, "user2") testNewIssue(t, session, "user2", "repo1", "Title", "Description")} func TestIssueCheckboxes(t *testing.T) { defer tests.PrepareTestEnv(t)() session := loginUser(t, "user2") issueURL := testNewIssue(t, session, "user2", "repo1", "Title", `- [x] small x- [X] capital X- [ ] empty - [x]x without gap - [ ]empty without gap- [x]x on new line- [ ]empty on new line - [ ] tabs instead of spacesDescription`) req := NewRequest(t, "GET", issueURL) resp := session.MakeRequest(t, req, http.StatusOK) issueContent := NewHTMLParser(t, resp.Body).doc.Find(".comment .render-content").First() isCheckBox := func(i int, s *goquery.Selection) bool { typeVal, typeExists := s.Attr("type") return typeExists && typeVal == "checkbox" } isChecked := func(i int, s *goquery.Selection) bool { _, checkedExists := s.Attr("checked") return checkedExists } checkBoxes := issueContent.Find("input").FilterFunction(isCheckBox) assert.Equal(t, 8, checkBoxes.Length()) assert.Equal(t, 4, checkBoxes.FilterFunction(isChecked).Length()) // Issues list should show the correct numbers of checked and total checkboxes repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, "user2", "repo1") require.NoError(t, err) req = NewRequestf(t, "GET", "%s/issues", repo.Link()) resp = MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) issuesSelection := htmlDoc.Find("#issue-list .flex-item") assert.Equal(t, "4 / 8", strings.TrimSpace(issuesSelection.Find(".checklist").Text())) value, _ := issuesSelection.Find("progress").Attr("value") vmax, _ := issuesSelection.Find("progress").Attr("max") assert.Equal(t, "4", value) assert.Equal(t, "8", vmax)} func TestIssueDependencies(t *testing.T) { defer tests.PrepareTestEnv(t)() owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) session := loginUser(t, owner.Name) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue) repo, _, f := tests.CreateDeclarativeRepoWithOptions(t, owner, tests.DeclarativeRepoOptions{}) defer f() createIssue := func(t *testing.T, title string) api.Issue { t.Helper() urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues", owner.Name, repo.Name) req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateIssueOption{ Body: "", Title: title, }).AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusCreated) var apiIssue api.Issue DecodeJSON(t, resp, &apiIssue) return apiIssue } addDependency := func(t *testing.T, issue, dependency api.Issue) { t.Helper() urlStr := fmt.Sprintf("/%s/%s/issues/%d/dependency/add", owner.Name, repo.Name, issue.Index) req := NewRequestWithValues(t, "POST", urlStr, map[string]string{ "_csrf": GetCSRF(t, session, fmt.Sprintf("/%s/%s/issues/%d", owner.Name, repo.Name, issue.Index)), "newDependency": fmt.Sprintf("%d", dependency.Index), }) session.MakeRequest(t, req, http.StatusSeeOther) } removeDependency := func(t *testing.T, issue, dependency api.Issue) { t.Helper() urlStr := fmt.Sprintf("/%s/%s/issues/%d/dependency/delete", owner.Name, repo.Name, issue.Index) req := NewRequestWithValues(t, "POST", urlStr, map[string]string{ "_csrf": GetCSRF(t, session, fmt.Sprintf("/%s/%s/issues/%d", owner.Name, repo.Name, issue.Index)), "removeDependencyID": fmt.Sprintf("%d", dependency.Index), "dependencyType": "blockedBy", }) session.MakeRequest(t, req, http.StatusSeeOther) } assertHasDependency := func(t *testing.T, issueID, dependencyID int64, hasDependency bool) { t.Helper() urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/dependencies", owner.Name, repo.Name, issueID) req := NewRequest(t, "GET", urlStr) resp := MakeRequest(t, req, http.StatusOK) var issues []api.Issue DecodeJSON(t, resp, &issues) if hasDependency { assert.NotEmpty(t, issues) assert.Equal(t, issues[0].Index, dependencyID) } else { assert.Empty(t, issues) }Lines 1–500 of 1483. Load more
// Copyright 2017 The Gitea Authors. All rights reserved. // Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. // SPDX-License-Identifier: MIT package integration import ( "fmt" "net/http" "net/http/httptest" "net/url" "path" "regexp" "strconv" "strings" "testing" "time" auth_model "forgejo.org/models/auth" "forgejo.org/models/db" issues_model "forgejo.org/models/issues" project_model "forgejo.org/models/project" repo_model "forgejo.org/models/repo" unit_model "forgejo.org/models/unit" "forgejo.org/models/unittest" user_model "forgejo.org/models/user" "forgejo.org/modules/indexer/issues" "forgejo.org/modules/optional" "forgejo.org/modules/references" "forgejo.org/modules/setting" api "forgejo.org/modules/structs" "forgejo.org/modules/test" "forgejo.org/modules/translation" files_service "forgejo.org/services/repository/files" user_service "forgejo.org/services/user" "forgejo.org/tests" "github.com/PuerkitoBio/goquery" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func getIssuesSelection(t testing.TB, htmlDoc *HTMLDoc) *goquery.Selection { issueList := htmlDoc.doc.Find("#issue-list") assert.Equal(t, 1, issueList.Length()) return issueList.Find(".flex-item").Find(".issue-title") } func getIssue(t *testing.T, repoID int64, issueSelection *goquery.Selection) *issues_model.Issue { href, exists := issueSelection.Attr("href") assert.True(t, exists) indexStr := href[strings.LastIndexByte(href, '/')+1:] index, err := strconv.Atoi(indexStr) require.NoError(t, err, "Invalid issue href: %s", href) return unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: repoID, Index: int64(index)}) } func assertMatch(t testing.TB, issue *issues_model.Issue, keyword string) { matches := strings.Contains(strings.ToLower(issue.Title), keyword) || strings.Contains(strings.ToLower(issue.Content), keyword) for _, comment := range issue.Comments { matches = matches || strings.Contains( strings.ToLower(comment.Content), keyword, ) } assert.True(t, matches) } func TestNoLoginViewIssues(t *testing.T) { defer tests.PrepareTestEnv(t)() req := NewRequest(t, "GET", "/user2/repo1/issues") MakeRequest(t, req, http.StatusOK) } func TestViewIssues(t *testing.T) { defer tests.PrepareTestEnv(t)() req := NewRequest(t, "GET", "/user2/repo1/issues") resp := MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) search := htmlDoc.doc.Find(".list-header-search > .search > .input > input") placeholder, _ := search.Attr("placeholder") assert.Equal(t, "Search issues…", placeholder) } func TestViewIssuesSortByType(t *testing.T) { defer tests.PrepareTestEnv(t)() user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) session := loginUser(t, user.Name) req := NewRequest(t, "GET", repo.Link()+"/issues?type=created_by") resp := session.MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) issuesSelection := getIssuesSelection(t, htmlDoc) expectedNumIssues := unittest.GetCount(t, &issues_model.Issue{RepoID: repo.ID, PosterID: user.ID}, unittest.Cond("is_closed=?", false), unittest.Cond("is_pull=?", false), ) if expectedNumIssues > setting.UI.IssuePagingNum { expectedNumIssues = setting.UI.IssuePagingNum } assert.Equal(t, expectedNumIssues, issuesSelection.Length()) issuesSelection.Each(func(_ int, selection *goquery.Selection) { issue := getIssue(t, repo.ID, selection) assert.Equal(t, user.ID, issue.PosterID) }) } func TestViewIssuesKeyword(t *testing.T) { defer tests.PrepareTestEnv(t)() repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ RepoID: repo.ID, Index: 1, }) issues.UpdateIssueIndexer(t.Context(), issue.ID) time.Sleep(time.Second * 1) const keyword = "first" req := NewRequestf(t, "GET", "%s/issues?q=%s", repo.Link(), keyword) resp := MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) issuesSelection := getIssuesSelection(t, htmlDoc) assert.Equal(t, 1, issuesSelection.Length()) issuesSelection.Each(func(_ int, selection *goquery.Selection) { issue := getIssue(t, repo.ID, selection) assert.False(t, issue.IsClosed) assert.False(t, issue.IsPull) assertMatch(t, issue, keyword) }) // keyword: 'firstt' // should not match when using phrase search req = NewRequestf(t, "GET", "%s/issues?q=\"%st\"", repo.Link(), keyword) resp = MakeRequest(t, req, http.StatusOK) htmlDoc = NewHTMLParser(t, resp.Body) issuesSelection = getIssuesSelection(t, htmlDoc) assert.Equal(t, 0, issuesSelection.Length()) // should match as 'first' when using a standard query req = NewRequestf(t, "GET", "%s/issues?q=%st", repo.Link(), keyword) resp = MakeRequest(t, req, http.StatusOK) htmlDoc = NewHTMLParser(t, resp.Body) issuesSelection = getIssuesSelection(t, htmlDoc) assert.Equal(t, 1, issuesSelection.Length()) issuesSelection.Each(func(_ int, selection *goquery.Selection) { issue := getIssue(t, repo.ID, selection) assert.False(t, issue.IsClosed) assert.False(t, issue.IsPull) assertMatch(t, issue, keyword) }) } func TestViewIssuesSearchOptions(t *testing.T) { defer tests.PrepareTestEnv(t)() repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) // there are two issues in repo1, both bound to a project. Add one // that is not bound to any project. _, issueNoProject := testIssueWithBean(t, "user2", 1, "Title", "Description") t.Run("All issues", func(t *testing.T) { req := NewRequestf(t, "GET", "%s/issues?state=all", repo.Link()) resp := MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) issuesSelection := getIssuesSelection(t, htmlDoc) assert.Equal(t, 3, issuesSelection.Length()) }) t.Run("Issues with no project", func(t *testing.T) { req := NewRequestf(t, "GET", "%s/issues?state=all&project=-1", repo.Link()) resp := MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) issuesSelection := getIssuesSelection(t, htmlDoc) assert.Equal(t, 1, issuesSelection.Length()) issuesSelection.Each(func(_ int, selection *goquery.Selection) { issue := getIssue(t, repo.ID, selection) assert.Equal(t, issueNoProject.ID, issue.ID) }) }) t.Run("Issues with a specific project", func(t *testing.T) { project := unittest.AssertExistsAndLoadBean(t, &project_model.Project{ID: 1}) req := NewRequestf(t, "GET", "%s/issues?state=all&project=%d", repo.Link(), project.ID) resp := MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) issuesSelection := getIssuesSelection(t, htmlDoc) assert.Equal(t, 2, issuesSelection.Length()) found := map[int64]bool{ 1: false, 5: false, } issuesSelection.Each(func(_ int, selection *goquery.Selection) { issue := getIssue(t, repo.ID, selection) found[issue.ID] = true }) assert.Len(t, found, 2) assert.True(t, found[1]) assert.True(t, found[5]) }) } func TestNoLoginViewIssue(t *testing.T) { defer tests.PrepareTestEnv(t)() req := NewRequest(t, "GET", "/user2/repo1/issues/1") MakeRequest(t, req, http.StatusOK) } func TestViewIssueCommentBox(t *testing.T) { defer tests.PrepareTestEnv(t)() locale := translation.NewLocale("en-US") repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) collaborator := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) viewer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) ownerSession := loginUser(t, owner.Name) collaboratorSession := loginUser(t, collaborator.Name) viewerSession := loginUser(t, viewer.Name) openIssueURL, _ := testIssueWithBean(t, owner.Name, repo.ID, "Open", "Description") warningSignin := string(locale.Tr("repo.issues.sign_in_require_desc", fmt.Sprintf("/user/login?redirect_to=%s", url.QueryEscape(openIssueURL)))) warningLocked := locale.TrString("discussion.locked") warningBlocked := locale.TrString("repo.comment.blocked_by_user") assertCommentForm := func(session *TestSession, url string, shouldExist bool) *HTMLDoc { var resp *httptest.ResponseRecorder req := NewRequest(t, "GET", url) if session != nil { resp = session.MakeRequest(t, req, http.StatusOK) } else { resp = MakeRequest(t, req, http.StatusOK) } htmlDoc := NewHTMLParser(t, resp.Body) htmlDoc.AssertElement(t, ".issue-content #comment-form", shouldExist) return htmlDoc } assertWarning := func(session *TestSession, url, warning string) { htmlDoc := assertCommentForm(session, url, false) timeline := htmlDoc.doc.Find(".issue-content .ui.timeline").Children() assert.Positivef(t, timeline.Length(), "There should be at least one entry in the timeline") html, _ := timeline.Last().Html() assert.Equal(t, strings.TrimSpace(html), warning) } t.Run("Owner can see comment box", func(t *testing.T) { assertCommentForm(ownerSession, openIssueURL, true) }) t.Run("Collaborator can see comment box", func(t *testing.T) { assertCommentForm(collaboratorSession, openIssueURL, true) }) t.Run("Viewer can see comment box", func(t *testing.T) { assertCommentForm(viewerSession, openIssueURL, true) }) t.Run("Anonymous will see singin warning", func(t *testing.T) { assertWarning(nil, openIssueURL, warningSignin) }) lockedIssueURL, lockedIssue := testIssueWithBean(t, owner.Name, repo.ID, "Locked", "Description") // Lock the issue req := NewRequest(t, "GET", lockedIssueURL) resp := ownerSession.MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) req = NewRequestWithValues(t, "POST", fmt.Sprintf("%s/lock", lockedIssueURL), map[string]string{ "_csrf": htmlDoc.GetCSRF(), "reason": "Too heated", }) _ = ownerSession.MakeRequest(t, req, http.StatusOK) lockedIssue = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: lockedIssue.ID}) assert.True(t, lockedIssue.IsLocked) t.Run("Owner can see comment box in locked issue", func(t *testing.T) { assertCommentForm(ownerSession, lockedIssueURL, true) }) t.Run("Collaborator can see comment box in locked issue", func(t *testing.T) { assertCommentForm(collaboratorSession, lockedIssueURL, true) }) t.Run("Viewer will see warning in locked issue", func(t *testing.T) { assertWarning(viewerSession, lockedIssueURL, warningLocked) }) t.Run("Anonymous will see warning in locked issue", func(t *testing.T) { assertWarning(nil, lockedIssueURL, warningLocked) }) err := user_service.BlockUser(db.DefaultContext, owner.ID, viewer.ID) require.NoError(t, err) t.Run("Blocked viewer will see warning", func(t *testing.T) { assertWarning(viewerSession, lockedIssueURL, warningBlocked) assertWarning(viewerSession, openIssueURL, warningBlocked) }) } func testNewIssue(t *testing.T, session *TestSession, user, repo, title, content string) string { req := NewRequest(t, "GET", path.Join(user, repo, "issues", "new")) resp := session.MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) link, exists := htmlDoc.doc.Find("form.ui.form").Attr("action") assert.True(t, exists, "The template has changed") req = NewRequestWithValues(t, "POST", link, map[string]string{ "_csrf": htmlDoc.GetCSRF(), "title": title, "content": content, }) resp = session.MakeRequest(t, req, http.StatusOK) issueURL := test.RedirectURL(resp) req = NewRequest(t, "GET", issueURL) resp = session.MakeRequest(t, req, http.StatusOK) htmlDoc = NewHTMLParser(t, resp.Body) val := htmlDoc.doc.Find("#issue-title-display").Text() assert.Contains(t, val, title) // test for first line only and if it contains only letters and spaces contentFirstLine := strings.Split(content, "\n")[0] patNotLetterOrSpace := regexp.MustCompile(`[^\p{L}\s]`) if len(contentFirstLine) != 0 && !patNotLetterOrSpace.MatchString(contentFirstLine) { val = htmlDoc.doc.Find(".comment .render-content p").First().Text() assert.Equal(t, contentFirstLine, val) } return issueURL } func testIssueAddComment(t *testing.T, session *TestSession, issueURL, content, status string) int64 { req := NewRequest(t, "GET", issueURL) resp := session.MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) link, exists := htmlDoc.doc.Find("#comment-form").Attr("action") assert.True(t, exists, "The template has changed") commentCount := htmlDoc.doc.Find(".comment-list .comment .render-content").Length() req = NewRequestWithValues(t, "POST", link, map[string]string{ "_csrf": htmlDoc.GetCSRF(), "content": content, "status": status, }) resp = session.MakeRequest(t, req, http.StatusOK) req = NewRequest(t, "GET", test.RedirectURL(resp)) resp = session.MakeRequest(t, req, http.StatusOK) htmlDoc = NewHTMLParser(t, resp.Body) val := htmlDoc.doc.Find(".comment-list .comment .render-content p").Eq(commentCount).Text() assert.Equal(t, content, val) idAttr, has := htmlDoc.doc.Find(".comment-list .comment").Eq(commentCount).Attr("id") idStr := idAttr[strings.LastIndexByte(idAttr, '-')+1:] assert.True(t, has) id, err := strconv.Atoi(idStr) require.NoError(t, err) return int64(id) } func TestNewIssue(t *testing.T) { defer tests.PrepareTestEnv(t)() session := loginUser(t, "user2") testNewIssue(t, session, "user2", "repo1", "Title", "Description") } func TestIssueCheckboxes(t *testing.T) { defer tests.PrepareTestEnv(t)() session := loginUser(t, "user2") issueURL := testNewIssue(t, session, "user2", "repo1", "Title", `- [x] small x - [X] capital X - [ ] empty - [x]x without gap - [ ]empty without gap - [x] x on new line - [ ] empty on new line - [ ] tabs instead of spaces Description`) req := NewRequest(t, "GET", issueURL) resp := session.MakeRequest(t, req, http.StatusOK) issueContent := NewHTMLParser(t, resp.Body).doc.Find(".comment .render-content").First() isCheckBox := func(i int, s *goquery.Selection) bool { typeVal, typeExists := s.Attr("type") return typeExists && typeVal == "checkbox" } isChecked := func(i int, s *goquery.Selection) bool { _, checkedExists := s.Attr("checked") return checkedExists } checkBoxes := issueContent.Find("input").FilterFunction(isCheckBox) assert.Equal(t, 8, checkBoxes.Length()) assert.Equal(t, 4, checkBoxes.FilterFunction(isChecked).Length()) // Issues list should show the correct numbers of checked and total checkboxes repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, "user2", "repo1") require.NoError(t, err) req = NewRequestf(t, "GET", "%s/issues", repo.Link()) resp = MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) issuesSelection := htmlDoc.Find("#issue-list .flex-item") assert.Equal(t, "4 / 8", strings.TrimSpace(issuesSelection.Find(".checklist").Text())) value, _ := issuesSelection.Find("progress").Attr("value") vmax, _ := issuesSelection.Find("progress").Attr("max") assert.Equal(t, "4", value) assert.Equal(t, "8", vmax) } func TestIssueDependencies(t *testing.T) { defer tests.PrepareTestEnv(t)() owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) session := loginUser(t, owner.Name) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue) repo, _, f := tests.CreateDeclarativeRepoWithOptions(t, owner, tests.DeclarativeRepoOptions{}) defer f() createIssue := func(t *testing.T, title string) api.Issue { t.Helper() urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues", owner.Name, repo.Name) req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateIssueOption{ Body: "", Title: title, }).AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusCreated) var apiIssue api.Issue DecodeJSON(t, resp, &apiIssue) return apiIssue } addDependency := func(t *testing.T, issue, dependency api.Issue) { t.Helper() urlStr := fmt.Sprintf("/%s/%s/issues/%d/dependency/add", owner.Name, repo.Name, issue.Index) req := NewRequestWithValues(t, "POST", urlStr, map[string]string{ "_csrf": GetCSRF(t, session, fmt.Sprintf("/%s/%s/issues/%d", owner.Name, repo.Name, issue.Index)), "newDependency": fmt.Sprintf("%d", dependency.Index), }) session.MakeRequest(t, req, http.StatusSeeOther) } removeDependency := func(t *testing.T, issue, dependency api.Issue) { t.Helper() urlStr := fmt.Sprintf("/%s/%s/issues/%d/dependency/delete", owner.Name, repo.Name, issue.Index) req := NewRequestWithValues(t, "POST", urlStr, map[string]string{ "_csrf": GetCSRF(t, session, fmt.Sprintf("/%s/%s/issues/%d", owner.Name, repo.Name, issue.Index)), "removeDependencyID": fmt.Sprintf("%d", dependency.Index), "dependencyType": "blockedBy", }) session.MakeRequest(t, req, http.StatusSeeOther) } assertHasDependency := func(t *testing.T, issueID, dependencyID int64, hasDependency bool) { t.Helper() urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/dependencies", owner.Name, repo.Name, issueID) req := NewRequest(t, "GET", urlStr) resp := MakeRequest(t, req, http.StatusOK) var issues []api.Issue DecodeJSON(t, resp, &issues) if hasDependency { assert.NotEmpty(t, issues) assert.Equal(t, issues[0].Index, dependencyID) } else { assert.Empty(t, issues) }