1483 lines · 51.3 KB · Go
RawDownload
// 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.