mirror of
https://github.com/go-gitea/gitea.git
synced 2026-11-13 12:28:02 +00:00
Adds a repository-level **"Restrict to collaborators"** option under the Issues and Pull Request unit settings. When enabled, only users with collaborator access to the base repository (owner, team members, or explicit collaborators) can open issues or pull requests against it. Public repositories often need to be visible/cloneable by everyone, but owners may want to limit who can open pull requests (e.g. restrict to an internal team while still keeping the code public) * Fixes: https://github.com/go-gitea/gitea/issues/40015 * Fixes: https://github.com/go-gitea/gitea/issues/31890 * Fixes: https://github.com/go-gitea/gitea/issues/28021 * Fixes: https://github.com/go-gitea/gitea/issues/14801 --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
783 lines
30 KiB
Go
783 lines
30 KiB
Go
// Copyright 2017 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package integration
|
|
|
|
import (
|
|
"fmt"
|
|
"html/template"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.dev/models/db"
|
|
issues_model "gitea.dev/models/issues"
|
|
repo_model "gitea.dev/models/repo"
|
|
"gitea.dev/models/unit"
|
|
"gitea.dev/models/unittest"
|
|
user_model "gitea.dev/models/user"
|
|
"gitea.dev/modules/indexer/issues"
|
|
"gitea.dev/modules/references"
|
|
"gitea.dev/modules/setting"
|
|
api "gitea.dev/modules/structs"
|
|
"gitea.dev/modules/test"
|
|
"gitea.dev/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(".item").Find(".list-item-large-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)
|
|
assert.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 TestIssueListButtons(t *testing.T) {
|
|
defer tests.PrepareTestEnv(t)()
|
|
|
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
|
issueUnit := unittest.AssertExistsAndLoadBean(t, &repo_model.RepoUnit{RepoID: repo.ID, Type: unit.TypeIssues})
|
|
session := loginUser(t, "user2")
|
|
for _, tc := range []struct {
|
|
name string
|
|
list string
|
|
signedIn bool
|
|
archived bool
|
|
restriction string
|
|
button string
|
|
}{
|
|
{"issue owner", "issues", true, false, "reader", "New Issue"},
|
|
{"issue anonymous", "issues", false, false, "reader", "New Issue"},
|
|
{"issue restricted", "issues", false, false, "writer", ""},
|
|
{"issue archived", "issues", true, true, "reader", ""},
|
|
{"pull owner", "pulls", true, false, "reader", "New Pull Request"},
|
|
{"pull anonymous", "pulls", false, false, "reader", "Compare commits"},
|
|
{"pull restricted", "pulls", false, false, "writer", "Compare commits"},
|
|
// {"pull archived", "pulls", true, true, "reader", ""}, // TODO: the existing behavior is using 404 for archived pulls, maybe it's not right
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
repo.IsArchived = tc.archived
|
|
require.NoError(t, repo_model.UpdateRepositoryColsNoAutoTime(t.Context(), repo, "is_archived"))
|
|
issueUnit.IssuesConfig().CreationRestriction = tc.restriction
|
|
require.NoError(t, repo_model.UpdateRepoUnitConfig(t.Context(), issueUnit))
|
|
updateRepoPullRequestConfig(t, repo.ID, func(c *repo_model.PullRequestsConfig) { c.CreationRestriction = tc.restriction })
|
|
|
|
req := NewRequest(t, "GET", "/user2/repo1/"+tc.list)
|
|
requestSession := emptyTestSession(t)
|
|
if tc.signedIn {
|
|
requestSession = session
|
|
}
|
|
resp := requestSession.MakeRequest(t, req, http.StatusOK)
|
|
button := NewHTMLParser(t, resp.Body).doc.Find(".list-header .issue-list-new")
|
|
if tc.button == "" {
|
|
assert.Zero(t, button.Length())
|
|
} else {
|
|
require.Equal(t, 1, button.Length())
|
|
assert.Equal(t, tc.button, strings.TrimSpace(button.Text()))
|
|
assert.False(t, button.HasClass("disabled"))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIssueViews(t *testing.T) {
|
|
defer tests.PrepareTestEnv(t)()
|
|
|
|
t.Run("NoLoginViewIssues", func(t *testing.T) {
|
|
req := NewRequest(t, "GET", "/user2/repo1/issues")
|
|
resp := MakeRequest(t, req, http.StatusOK)
|
|
htmlDoc := NewHTMLParser(t, resp.Body)
|
|
issue := htmlDoc.doc.Find("#issue-list .list-item-large-title[href='/user2/repo1/issues/1']").Closest(".item")
|
|
assert.Equal(t, "label1", strings.TrimSpace(issue.Find(".labels-list").Text()))
|
|
assignee := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
|
assert.Equal(t, assignee.HomeLink(), issue.Find(".assignee").AttrOr("href", ""))
|
|
assert.Equal(t, assignee.GetDisplayName(), issue.Find(".assignee").AttrOr("data-tooltip-content", ""))
|
|
})
|
|
|
|
t.Run("ViewIssueListContexts", func(t *testing.T) {
|
|
session := loginUser(t, "user2")
|
|
t.Run("dashboard", func(t *testing.T) {
|
|
req := NewRequest(t, "GET", "/issues")
|
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
|
htmlDoc := NewHTMLParser(t, resp.Body)
|
|
assert.Equal(t, "user2/repo1#1", strings.TrimSpace(htmlDoc.doc.Find("#issue-list .index[href='/user2/repo1/issues/1']").Text()))
|
|
})
|
|
t.Run("milestone", func(t *testing.T) {
|
|
req := NewRequest(t, "GET", "/user2/repo1/milestone/1")
|
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
|
htmlDoc := NewHTMLParser(t, resp.Body)
|
|
assert.Equal(t, 1, getIssuesSelection(t, htmlDoc).Length())
|
|
assert.Empty(t, htmlDoc.doc.Find("#issue-list .milestone").Nodes)
|
|
labels := htmlDoc.doc.Find("#issue-list .labels-list a")
|
|
assert.Equal(t, 2, labels.Length())
|
|
labels.Each(func(_ int, label *goquery.Selection) {
|
|
link, err := url.Parse(label.AttrOr("href", ""))
|
|
assert.NoError(t, err)
|
|
assert.NotContains(t, link.Query(), "milestone")
|
|
})
|
|
})
|
|
})
|
|
|
|
t.Run("ViewIssuesSortByType", func(t *testing.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 := min(unittest.GetCount(t,
|
|
&issues_model.Issue{RepoID: repo.ID, PosterID: user.ID},
|
|
unittest.Cond("is_closed=?", false),
|
|
unittest.Cond("is_pull=?", false),
|
|
), 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)
|
|
})
|
|
})
|
|
|
|
t.Run("SearchIssues", func(t *testing.T) {
|
|
session := loginUser(t, "user2")
|
|
|
|
expectedIssueCount := min(20, setting.UI.IssuePagingNum) // 20 is from the fixtures
|
|
|
|
link, _ := url.Parse("/issues/search")
|
|
checkIssues := func(expectedCount int, expectedTotal string) {
|
|
req := NewRequest(t, "GET", link.String())
|
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
|
apiIssues := DecodeJSON(t, resp, []*api.Issue{})
|
|
if expectedTotal != "" {
|
|
assert.Equal(t, expectedTotal, resp.Header().Get("X-Total-Count"))
|
|
}
|
|
assert.Len(t, apiIssues, expectedCount)
|
|
}
|
|
checkIssues(expectedIssueCount, "")
|
|
|
|
since := "2000-01-01T00:50:01+00:00" // 946687801
|
|
before := time.Unix(999307200, 0).Format(time.RFC3339)
|
|
query := url.Values{}
|
|
query.Add("since", since)
|
|
query.Add("before", before)
|
|
link.RawQuery = query.Encode()
|
|
checkIssues(11, "")
|
|
query.Del("since")
|
|
query.Del("before")
|
|
|
|
query.Add("state", "closed")
|
|
link.RawQuery = query.Encode()
|
|
checkIssues(2, "")
|
|
|
|
query.Set("state", "all")
|
|
link.RawQuery = query.Encode()
|
|
checkIssues(20, "22")
|
|
|
|
query.Add("limit", "5")
|
|
link.RawQuery = query.Encode()
|
|
checkIssues(5, "22")
|
|
|
|
query = url.Values{"assigned": {"true"}, "state": {"all"}}
|
|
link.RawQuery = query.Encode()
|
|
checkIssues(2, "")
|
|
|
|
query = url.Values{"milestones": {"milestone1"}, "state": {"all"}}
|
|
link.RawQuery = query.Encode()
|
|
checkIssues(1, "")
|
|
|
|
query = url.Values{"milestones": {"milestone1,milestone3"}, "state": {"all"}}
|
|
link.RawQuery = query.Encode()
|
|
checkIssues(2, "")
|
|
|
|
query = url.Values{"owner": {"user2"}} // user
|
|
link.RawQuery = query.Encode()
|
|
checkIssues(8, "")
|
|
|
|
query = url.Values{"owner": {"org3"}} // organization
|
|
link.RawQuery = query.Encode()
|
|
checkIssues(5, "")
|
|
|
|
query = url.Values{"owner": {"org3"}, "team": {"team1"}} // organization + team
|
|
link.RawQuery = query.Encode()
|
|
checkIssues(2, "")
|
|
})
|
|
|
|
t.Run("SearchIssuesWithLabels", func(t *testing.T) {
|
|
expectedIssueCount := min(20, setting.UI.IssuePagingNum) // 20 is from the fixtures
|
|
|
|
session := loginUser(t, "user1")
|
|
link, _ := url.Parse("/issues/search")
|
|
query := url.Values{}
|
|
checkIssues := func(expectedCount int) {
|
|
req := NewRequest(t, "GET", link.String())
|
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
|
apiIssues := DecodeJSON(t, resp, []*api.Issue{})
|
|
assert.Len(t, apiIssues, expectedCount)
|
|
}
|
|
|
|
link.RawQuery = query.Encode()
|
|
checkIssues(expectedIssueCount)
|
|
|
|
query.Add("labels", "label1")
|
|
link.RawQuery = query.Encode()
|
|
checkIssues(2)
|
|
|
|
// multiple labels
|
|
query.Set("labels", "label1,label2")
|
|
link.RawQuery = query.Encode()
|
|
checkIssues(2)
|
|
|
|
// an org label
|
|
query.Set("labels", "orglabel4")
|
|
link.RawQuery = query.Encode()
|
|
checkIssues(1)
|
|
|
|
// org and repo label
|
|
query.Set("labels", "label2,orglabel4")
|
|
query.Add("state", "all")
|
|
link.RawQuery = query.Encode()
|
|
checkIssues(2)
|
|
|
|
// org and repo label which share the same issue
|
|
query.Set("labels", "label1,orglabel4")
|
|
link.RawQuery = query.Encode()
|
|
checkIssues(2)
|
|
})
|
|
|
|
t.Run("GetIssueInfo", func(t *testing.T) {
|
|
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 10})
|
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
|
|
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
|
assert.NoError(t, issue.LoadAttributes(t.Context()))
|
|
assert.Equal(t, int64(1019307200), int64(issue.DeadlineUnix))
|
|
assert.Equal(t, api.StateOpen, issue.State())
|
|
|
|
session := loginUser(t, owner.Name)
|
|
|
|
urlStr := fmt.Sprintf("/%s/%s/issues/%d/info", owner.Name, repo.Name, issue.Index)
|
|
req := NewRequest(t, "GET", urlStr)
|
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
|
respStruct := DecodeJSON(t, resp, &struct {
|
|
ConvertedIssue api.Issue
|
|
RenderedLabels template.HTML
|
|
}{})
|
|
|
|
assert.Equal(t, issue.ID, respStruct.ConvertedIssue.ID)
|
|
assert.Contains(t, string(respStruct.RenderedLabels), `"labels-list"`)
|
|
})
|
|
|
|
t.Run("IssueRefSelectorEnabledForNewIssue", func(t *testing.T) {
|
|
// user4 (non-collaborator on user2/repo1) must see an enabled ref selector
|
|
// when creating a new issue.
|
|
session := loginUser(t, "user4")
|
|
req := NewRequest(t, "GET", "/user2/repo1/issues/new")
|
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
|
htmlDoc := NewHTMLParser(t, resp.Body)
|
|
|
|
sel := htmlDoc.Find(".branch-selector-dropdown")
|
|
assert.Equal(t, 1, sel.Length())
|
|
assert.False(t, sel.HasClass("disabled"), "branch selector should be enabled on the new issue form")
|
|
})
|
|
|
|
t.Run("ViewIssuesKeyword", func(t *testing.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)
|
|
const keyword = "first"
|
|
require.Eventually(t, func() bool {
|
|
issueIDs, _, err := issues.SearchIssues(t.Context(), &issues.SearchOptions{Keyword: keyword, RepoIDs: []int64{repo.ID}})
|
|
return err == nil && slices.Contains(issueIDs, issue.ID)
|
|
}, 10*time.Second, 10*time.Millisecond)
|
|
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)
|
|
})
|
|
})
|
|
|
|
t.Run("NoLoginViewIssue", func(t *testing.T) {
|
|
req := NewRequest(t, "GET", "/user2/repo1/issues/1")
|
|
MakeRequest(t, req, http.StatusOK)
|
|
})
|
|
|
|
t.Run("IssueReferenceURL", func(t *testing.T) {
|
|
session := loginUser(t, "user2")
|
|
|
|
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
|
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
|
|
|
|
req := NewRequest(t, "GET", fmt.Sprintf("%s/issues/%d", repo.Link(), issue.Index))
|
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
|
htmlDoc := NewHTMLParser(t, resp.Body)
|
|
|
|
ref, _ := htmlDoc.Find(`.timeline-item.comment.issue-content-comment .reference-issue`).Attr("data-reference")
|
|
assert.Equal(t, setting.AppURL+"user2/repo1/issues/1#issue-1", ref)
|
|
|
|
ref, _ = htmlDoc.Find(`.timeline-item.comment:not(.issue-content-comment) .reference-issue`).Attr("data-reference")
|
|
assert.Equal(t, setting.AppURL+"user2/repo1/issues/1#issuecomment-2", ref)
|
|
})
|
|
|
|
t.Run("UpdateIssueDeadline", func(t *testing.T) {
|
|
issueBefore := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 10})
|
|
repoBefore := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issueBefore.RepoID})
|
|
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repoBefore.OwnerID})
|
|
assert.NoError(t, issueBefore.LoadAttributes(t.Context()))
|
|
assert.Equal(t, "2002-04-20", issueBefore.DeadlineUnix.FormatDate())
|
|
assert.Equal(t, api.StateOpen, issueBefore.State())
|
|
|
|
session := loginUser(t, owner.Name)
|
|
urlStr := fmt.Sprintf("/%s/%s/issues/%d/deadline", owner.Name, repoBefore.Name, issueBefore.Index)
|
|
|
|
req := NewRequestWithValues(t, "POST", urlStr, map[string]string{"deadline": "2022-04-06"})
|
|
session.MakeRequest(t, req, http.StatusOK)
|
|
issueAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 10})
|
|
assert.Equal(t, "2022-04-06", issueAfter.DeadlineUnix.FormatDate())
|
|
|
|
req = NewRequestWithValues(t, "POST", urlStr, map[string]string{"deadline": ""})
|
|
session.MakeRequest(t, req, http.StatusOK)
|
|
issueAfter = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 10})
|
|
assert.True(t, issueAfter.DeadlineUnix.IsZero())
|
|
})
|
|
|
|
t.Run("IssueRedirect", func(t *testing.T) {
|
|
session := loginUser(t, "user2")
|
|
assertRedirect := func(requestURL, expectedURL string) {
|
|
req := NewRequest(t, "GET", requestURL)
|
|
resp := session.MakeRequest(t, req, http.StatusSeeOther)
|
|
assert.Equal(t, expectedURL, test.RedirectURL(resp))
|
|
}
|
|
|
|
// Test external tracker where style not set (shall default numeric)
|
|
assertRedirect("/org26/repo_external_tracker/issues/1", "https://tracker.com/org26/repo_external_tracker/issues/1")
|
|
|
|
// Test external tracker with numeric style
|
|
assertRedirect("/org26/repo_external_tracker_numeric/issues/1", "https://tracker.com/org26/repo_external_tracker_numeric/issues/1")
|
|
|
|
// Test external tracker with alphanumeric style (for a pull request)
|
|
assertRedirect("/org26/repo_external_tracker_alpha/issues/1", "/org26/repo_external_tracker_alpha/pulls/1")
|
|
|
|
// test to check that the PR redirection works if the issue unit is disabled
|
|
// repo1 is a normal repository with issue unit enabled, visit issue 2(which is a pull request)
|
|
// will redirect to pulls
|
|
assertRedirect("/user2/repo1/issues/2", "/user2/repo1/pulls/2")
|
|
|
|
// by the way, test PR redirection
|
|
assertRedirect("/user2/repo1/pulls/1", "/user2/repo1/issues/1")
|
|
|
|
// disable issue unit
|
|
repoUnit := unittest.AssertExistsAndLoadBean(t, &repo_model.RepoUnit{RepoID: 1, Type: unit.TypeIssues})
|
|
_, err := db.DeleteByID[repo_model.RepoUnit](t.Context(), repoUnit.ID)
|
|
assert.NoError(t, err)
|
|
|
|
// even if the issue unit is disabled, visiting an issue which is a pull request
|
|
// will still redirect to pull request
|
|
assertRedirect("/user2/repo1/issues/2", "/user2/repo1/pulls/2")
|
|
})
|
|
}
|
|
|
|
func TestIssueIDsSortByLabelScope(t *testing.T) {
|
|
assert.NoError(t, unittest.LoadFixtures())
|
|
high := &issues_model.Label{RepoID: 1, Name: "priority/high", Exclusive: true, ExclusiveOrder: 1}
|
|
low := &issues_model.Label{RepoID: 1, Name: "priority/low", Exclusive: true, ExclusiveOrder: 2}
|
|
assert.NoError(t, db.Insert(t.Context(), high, low))
|
|
assert.NoError(t, db.Insert(t.Context(),
|
|
&issues_model.IssueLabel{IssueID: 1, LabelID: low.ID},
|
|
&issues_model.IssueLabel{IssueID: 2, LabelID: high.ID},
|
|
))
|
|
for page, expectedIDs := range [][]int64{{2, 1, 11}, {5, 3}} {
|
|
issueIDs, total, err := issues_model.IssueIDs(t.Context(), &issues_model.IssuesOptions{
|
|
RepoIDs: []int64{1},
|
|
SortType: issues_model.ScopeSortPrefix + "priority",
|
|
Paginator: &db.ListOptions{Page: page + 1, PageSize: 3},
|
|
})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, expectedIDs, issueIDs)
|
|
assert.EqualValues(t, 5, total)
|
|
}
|
|
}
|
|
|
|
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#new-issue").Attr("action")
|
|
assert.True(t, exists, "The template has changed")
|
|
req = NewRequestWithValues(t, "POST", link, map[string]string{
|
|
"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)
|
|
val = htmlDoc.doc.Find(".comment .render-content p").First().Text()
|
|
assert.Equal(t, content, val)
|
|
|
|
return issueURL
|
|
}
|
|
|
|
func testIssueDelete(t *testing.T, session *TestSession, issueURL string) {
|
|
req := NewRequest(t, "POST", path.Join(issueURL, "delete"))
|
|
session.MakeRequest(t, req, http.StatusSeeOther)
|
|
}
|
|
|
|
func testIssueAssign(t *testing.T, session *TestSession, repoLink string, issueID, assigneeID int64) {
|
|
req := NewRequestWithValues(t, "POST", fmt.Sprintf(repoLink+"/issues/assignee?issue_ids=%d", issueID), map[string]string{
|
|
"id": strconv.FormatInt(assigneeID, 10),
|
|
"action": "", // empty action means assign
|
|
})
|
|
session.MakeRequest(t, req, http.StatusOK)
|
|
}
|
|
|
|
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{
|
|
"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 := strings.TrimSpace(htmlDoc.doc.Find(".comment-list .comment .render-content").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)
|
|
assert.NoError(t, err)
|
|
return int64(id)
|
|
}
|
|
|
|
func testIssueChangeMilestone(t *testing.T, session *TestSession, repoLink string, issueID, milestoneID int64) {
|
|
req := NewRequestWithValues(t, "POST", fmt.Sprintf(repoLink+"/issues/milestone?issue_ids=%d", issueID), map[string]string{
|
|
"id": strconv.FormatInt(milestoneID, 10),
|
|
})
|
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
|
assert.Equal(t, `{"ok":true}`, strings.TrimSpace(resp.Body.String()))
|
|
}
|
|
|
|
func TestNewIssue(t *testing.T) {
|
|
defer tests.PrepareTestEnv(t)()
|
|
session := loginUser(t, "user2")
|
|
testNewIssue(t, session, "user2", "repo1", "Title", "Description")
|
|
}
|
|
|
|
func TestEditIssue(t *testing.T) {
|
|
defer tests.PrepareTestEnv(t)()
|
|
session := loginUser(t, "user2")
|
|
issueURL := testNewIssue(t, session, "user2", "repo1", "Title", "Description")
|
|
|
|
req := NewRequestWithValues(t, "POST", issueURL+"/content", map[string]string{
|
|
"content": "modified content",
|
|
"context": fmt.Sprintf("/%s/%s", "user2", "repo1"),
|
|
})
|
|
session.MakeRequest(t, req, http.StatusOK)
|
|
|
|
req = NewRequestWithValues(t, "POST", issueURL+"/content", map[string]string{
|
|
"content": "modified content",
|
|
"context": fmt.Sprintf("/%s/%s", "user2", "repo1"),
|
|
})
|
|
session.MakeRequest(t, req, http.StatusBadRequest)
|
|
|
|
req = NewRequestWithValues(t, "POST", issueURL+"/content", map[string]string{
|
|
"content": "modified content",
|
|
"content_version": "1",
|
|
"context": fmt.Sprintf("/%s/%s", "user2", "repo1"),
|
|
})
|
|
session.MakeRequest(t, req, http.StatusOK)
|
|
}
|
|
|
|
func TestIssueCommentClose(t *testing.T) {
|
|
defer tests.PrepareTestEnv(t)()
|
|
session := loginUser(t, "user2")
|
|
issueURL := testNewIssue(t, session, "user2", "repo1", "Title", "Description")
|
|
testIssueAddComment(t, session, issueURL, "Test comment 1", "")
|
|
testIssueAddComment(t, session, issueURL, "Test comment 2", "")
|
|
testIssueAddComment(t, session, issueURL, "Test comment 3", "close")
|
|
|
|
// Validate that issue content has not been updated
|
|
req := NewRequest(t, "GET", issueURL)
|
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
|
htmlDoc := NewHTMLParser(t, resp.Body)
|
|
val := htmlDoc.doc.Find(".comment-list .comment .render-content p").First().Text()
|
|
assert.Equal(t, "Description", val)
|
|
}
|
|
|
|
func TestIssueCommentDelete(t *testing.T) {
|
|
defer tests.PrepareTestEnv(t)()
|
|
session := loginUser(t, "user2")
|
|
issueURL := testNewIssue(t, session, "user2", "repo1", "Title", "Description")
|
|
comment1 := "Test comment 1"
|
|
commentID := testIssueAddComment(t, session, issueURL, comment1, "")
|
|
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: commentID})
|
|
assert.Equal(t, comment1, comment.Content)
|
|
|
|
// Using the ID of a comment that does not belong to the repository must fail
|
|
req := NewRequest(t, "POST", fmt.Sprintf("/%s/%s/comments/%d/delete", "user5", "repo4", commentID))
|
|
session.MakeRequest(t, req, http.StatusNotFound)
|
|
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/comments/%d/delete", "user2", "repo1", commentID))
|
|
session.MakeRequest(t, req, http.StatusOK)
|
|
unittest.AssertNotExistsBean(t, &issues_model.Comment{ID: commentID})
|
|
}
|
|
|
|
func TestIssueCommentUpdate(t *testing.T) {
|
|
defer tests.PrepareTestEnv(t)()
|
|
session := loginUser(t, "user2")
|
|
issueURL := testNewIssue(t, session, "user2", "repo1", "Title", "Description")
|
|
comment1 := "Test comment 1"
|
|
commentID := testIssueAddComment(t, session, issueURL, comment1, "")
|
|
|
|
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: commentID})
|
|
assert.Equal(t, comment1, comment.Content)
|
|
|
|
modifiedContent := comment.Content + "MODIFIED"
|
|
|
|
// Using the ID of a comment that does not belong to the repository must fail
|
|
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d", "user5", "repo4", commentID), map[string]string{
|
|
"content": modifiedContent,
|
|
})
|
|
session.MakeRequest(t, req, http.StatusNotFound)
|
|
|
|
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d", "user2", "repo1", commentID), map[string]string{
|
|
"content": modifiedContent,
|
|
})
|
|
session.MakeRequest(t, req, http.StatusOK)
|
|
|
|
comment = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: commentID})
|
|
assert.Equal(t, modifiedContent, comment.Content)
|
|
}
|
|
|
|
func TestIssueCommentUpdateSimultaneously(t *testing.T) {
|
|
defer tests.PrepareTestEnv(t)()
|
|
session := loginUser(t, "user2")
|
|
issueURL := testNewIssue(t, session, "user2", "repo1", "Title", "Description")
|
|
comment1 := "Test comment 1"
|
|
commentID := testIssueAddComment(t, session, issueURL, comment1, "")
|
|
|
|
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: commentID})
|
|
assert.Equal(t, comment1, comment.Content)
|
|
|
|
modifiedContent := comment.Content + "MODIFIED"
|
|
|
|
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d", "user2", "repo1", commentID), map[string]string{
|
|
"content": modifiedContent,
|
|
})
|
|
session.MakeRequest(t, req, http.StatusOK)
|
|
|
|
modifiedContent = comment.Content + "2"
|
|
|
|
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d", "user2", "repo1", commentID), map[string]string{
|
|
"content": modifiedContent,
|
|
})
|
|
session.MakeRequest(t, req, http.StatusBadRequest)
|
|
|
|
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d", "user2", "repo1", commentID), map[string]string{
|
|
"content": modifiedContent,
|
|
"content_version": "1",
|
|
})
|
|
session.MakeRequest(t, req, http.StatusOK)
|
|
|
|
comment = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: commentID})
|
|
assert.Equal(t, modifiedContent, comment.Content)
|
|
assert.Equal(t, 2, comment.ContentVersion)
|
|
}
|
|
|
|
func TestIssueReaction(t *testing.T) {
|
|
defer tests.PrepareTestEnv(t)()
|
|
session := loginUser(t, "user2")
|
|
issueURL := testNewIssue(t, session, "user2", "repo1", "Title", "Description")
|
|
|
|
req := NewRequestWithValues(t, "POST", path.Join(issueURL, "/reactions/react"), map[string]string{
|
|
"content": "8ball",
|
|
})
|
|
session.MakeRequest(t, req, http.StatusForbidden)
|
|
req = NewRequestWithValues(t, "POST", path.Join(issueURL, "/reactions/react"), map[string]string{
|
|
"content": "eyes",
|
|
})
|
|
session.MakeRequest(t, req, http.StatusOK)
|
|
req = NewRequestWithValues(t, "POST", path.Join(issueURL, "/reactions/unreact"), map[string]string{
|
|
"content": "eyes",
|
|
})
|
|
session.MakeRequest(t, req, http.StatusOK)
|
|
|
|
assert.NoError(t, issues_model.UpdateIssueCols(t.Context(), &issues_model.Issue{ID: 1, IsLocked: true}, "is_locked"))
|
|
readerSession := loginUser(t, "user4")
|
|
req = NewRequestWithValues(t, "POST", "/user2/repo1/issues/1/reactions/react", map[string]string{"content": "eyes"})
|
|
readerSession.MakeRequest(t, req, http.StatusForbidden)
|
|
req = NewRequestWithValues(t, "POST", "/user2/repo1/comments/2/reactions/react", map[string]string{"content": "eyes"})
|
|
readerSession.MakeRequest(t, req, http.StatusForbidden)
|
|
req = NewRequestWithValues(t, "POST", "/user2/repo1/comments/2/reactions/react", map[string]string{"content": "eyes"})
|
|
session.MakeRequest(t, req, http.StatusOK)
|
|
}
|
|
|
|
func TestIssueCrossReference(t *testing.T) {
|
|
defer tests.PrepareTestEnv(t)()
|
|
|
|
// Issue that will be referenced
|
|
_, issueBase := testIssueWithBean(t, "user2", 1, "Title", "Description")
|
|
assertReference := func(repoID, issueID, commentID int64, action references.XRefAction) {
|
|
unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{
|
|
IssueID: issueBase.ID,
|
|
RefRepoID: repoID,
|
|
RefIssueID: issueID,
|
|
RefCommentID: commentID,
|
|
RefIsPull: false,
|
|
RefAction: action,
|
|
})
|
|
}
|
|
|
|
// Ref from issue title
|
|
issueRefURL, issueRef := testIssueWithBean(t, "user2", 1, fmt.Sprintf("Title ref #%d", issueBase.Index), "Description")
|
|
assertReference(1, issueRef.ID, 0, references.XRefActionNone)
|
|
|
|
// Edit title, neuter ref
|
|
testIssueChangeInfo(t, "user2", issueRefURL, "title", "Title no ref")
|
|
assertReference(1, issueRef.ID, 0, references.XRefActionNeutered)
|
|
|
|
// Ref from issue content
|
|
issueRefURL, issueRef = testIssueWithBean(t, "user2", 1, "TitleXRef", fmt.Sprintf("Description ref #%d", issueBase.Index))
|
|
assertReference(1, issueRef.ID, 0, references.XRefActionNone)
|
|
|
|
// Edit content, neuter ref
|
|
testIssueChangeInfo(t, "user2", issueRefURL, "content", "Description no ref")
|
|
assertReference(1, issueRef.ID, 0, references.XRefActionNeutered)
|
|
|
|
// Ref from a comment
|
|
session := loginUser(t, "user2")
|
|
commentID := testIssueAddComment(t, session, issueRefURL, fmt.Sprintf("Adding ref from comment #%d", issueBase.Index), "")
|
|
assertReference(1, issueRef.ID, commentID, references.XRefActionNone)
|
|
|
|
// Ref from a different repository
|
|
_, issueRef = testIssueWithBean(t, "user12", 10, "TitleXRef", fmt.Sprintf("Description ref user2/repo1#%d", issueBase.Index))
|
|
assertReference(10, issueRef.ID, 0, references.XRefActionNone)
|
|
}
|
|
|
|
func testIssueWithBean(t *testing.T, user string, repoID int64, title, content string) (string, *issues_model.Issue) {
|
|
session := loginUser(t, user)
|
|
issueURL := testNewIssue(t, session, user, fmt.Sprintf("repo%d", repoID), title, content)
|
|
indexStr := issueURL[strings.LastIndexByte(issueURL, '/')+1:]
|
|
index, err := strconv.Atoi(indexStr)
|
|
assert.NoError(t, err, "Invalid issue href: %s", issueURL)
|
|
issue := &issues_model.Issue{RepoID: repoID, Index: int64(index)}
|
|
unittest.AssertExistsAndLoadBean(t, issue)
|
|
return issueURL, issue
|
|
}
|
|
|
|
func testIssueChangeInfo(t *testing.T, user, issueURL, info, value string) {
|
|
session := loginUser(t, user)
|
|
req := NewRequestWithValues(t, "POST", path.Join(issueURL, info), map[string]string{
|
|
info: value,
|
|
})
|
|
_ = session.MakeRequest(t, req, http.StatusOK)
|
|
}
|
|
|
|
func TestUpdateIssueRefByPoster(t *testing.T) {
|
|
defer tests.PrepareTestEnv(t)()
|
|
|
|
// user4 is a non-admin, non-collaborator on user2/repo1.
|
|
// They create an issue, making them the poster.
|
|
posterSession := loginUser(t, "user4")
|
|
issueURL := testNewIssue(t, posterSession, "user2", "repo1", "Poster ref test", "body")
|
|
refURL := issueURL + "/ref"
|
|
|
|
// The poster (non-collaborator) must be able to update the ref.
|
|
req := NewRequestWithValues(t, "POST", refURL, map[string]string{"ref": "refs/heads/main"})
|
|
posterSession.MakeRequest(t, req, http.StatusOK)
|
|
|
|
// A different non-collaborator non-poster must be forbidden.
|
|
otherSession := loginUser(t, "user5")
|
|
req = NewRequestWithValues(t, "POST", refURL, map[string]string{"ref": "refs/heads/main"})
|
|
otherSession.MakeRequest(t, req, http.StatusForbidden)
|
|
}
|
|
|
|
func TestIssueRefSelectorEnabledForPoster(t *testing.T) {
|
|
defer tests.PrepareTestEnv(t)()
|
|
|
|
// user4 creates an issue in user2/repo1 (user4 has no write permission there).
|
|
posterSession := loginUser(t, "user4")
|
|
issueURL := testNewIssue(t, posterSession, "user2", "repo1", "Ref selector test", "body")
|
|
|
|
resp := posterSession.MakeRequest(t, NewRequest(t, "GET", issueURL), http.StatusOK)
|
|
htmlDoc := NewHTMLParser(t, resp.Body)
|
|
|
|
// The branch selector must not carry the "disabled" CSS class for the poster.
|
|
sel := htmlDoc.Find(".branch-selector-dropdown")
|
|
assert.Equal(t, 1, sel.Length())
|
|
assert.False(t, sel.HasClass("disabled"), "branch selector should be enabled for the issue poster")
|
|
// The update-ref URL must be present so JS can send the POST request.
|
|
_, hasURL := sel.Attr("data-url-update-issueref")
|
|
assert.True(t, hasURL, "data-url-update-issueref must be set for the issue poster")
|
|
}
|
|
|
|
func TestIssueCommentContent(t *testing.T) {
|
|
defer tests.PrepareTestEnv(t)()
|
|
|
|
issue := &issues_model.Issue{Title: "title\x00content", Content: "body\x00content"}
|
|
require.NoError(t, db.Insert(t.Context(), issue))
|
|
unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issue.ID, Title: "title content", Content: "body content"})
|
|
|
|
comment := &issues_model.Comment{Content: "body\x00content"}
|
|
require.NoError(t, db.Insert(t.Context(), comment))
|
|
unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: comment.ID, Content: "body content"})
|
|
}
|