gitea源码

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730
  1. // Copyright 2017 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package integration
  4. import (
  5. "fmt"
  6. "html/template"
  7. "net/http"
  8. "net/url"
  9. "path"
  10. "strconv"
  11. "strings"
  12. "testing"
  13. "time"
  14. "code.gitea.io/gitea/models/db"
  15. issues_model "code.gitea.io/gitea/models/issues"
  16. repo_model "code.gitea.io/gitea/models/repo"
  17. "code.gitea.io/gitea/models/unit"
  18. "code.gitea.io/gitea/models/unittest"
  19. user_model "code.gitea.io/gitea/models/user"
  20. "code.gitea.io/gitea/modules/indexer/issues"
  21. "code.gitea.io/gitea/modules/references"
  22. "code.gitea.io/gitea/modules/setting"
  23. api "code.gitea.io/gitea/modules/structs"
  24. "code.gitea.io/gitea/modules/test"
  25. "code.gitea.io/gitea/tests"
  26. "github.com/PuerkitoBio/goquery"
  27. "github.com/stretchr/testify/assert"
  28. )
  29. func getIssuesSelection(t testing.TB, htmlDoc *HTMLDoc) *goquery.Selection {
  30. issueList := htmlDoc.doc.Find("#issue-list")
  31. assert.Equal(t, 1, issueList.Length())
  32. return issueList.Find(".flex-item").Find(".issue-title")
  33. }
  34. func getIssue(t *testing.T, repoID int64, issueSelection *goquery.Selection) *issues_model.Issue {
  35. href, exists := issueSelection.Attr("href")
  36. assert.True(t, exists)
  37. indexStr := href[strings.LastIndexByte(href, '/')+1:]
  38. index, err := strconv.Atoi(indexStr)
  39. assert.NoError(t, err, "Invalid issue href: %s", href)
  40. return unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: repoID, Index: int64(index)})
  41. }
  42. func assertMatch(t testing.TB, issue *issues_model.Issue, keyword string) {
  43. matches := strings.Contains(strings.ToLower(issue.Title), keyword) ||
  44. strings.Contains(strings.ToLower(issue.Content), keyword)
  45. for _, comment := range issue.Comments {
  46. matches = matches || strings.Contains(
  47. strings.ToLower(comment.Content),
  48. keyword,
  49. )
  50. }
  51. assert.True(t, matches)
  52. }
  53. func TestNoLoginViewIssues(t *testing.T) {
  54. defer tests.PrepareTestEnv(t)()
  55. req := NewRequest(t, "GET", "/user2/repo1/issues")
  56. MakeRequest(t, req, http.StatusOK)
  57. }
  58. func TestViewIssuesSortByType(t *testing.T) {
  59. defer tests.PrepareTestEnv(t)()
  60. user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
  61. repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
  62. session := loginUser(t, user.Name)
  63. req := NewRequest(t, "GET", repo.Link()+"/issues?type=created_by")
  64. resp := session.MakeRequest(t, req, http.StatusOK)
  65. htmlDoc := NewHTMLParser(t, resp.Body)
  66. issuesSelection := getIssuesSelection(t, htmlDoc)
  67. expectedNumIssues := min(unittest.GetCount(t,
  68. &issues_model.Issue{RepoID: repo.ID, PosterID: user.ID},
  69. unittest.Cond("is_closed=?", false),
  70. unittest.Cond("is_pull=?", false),
  71. ), setting.UI.IssuePagingNum)
  72. assert.Equal(t, expectedNumIssues, issuesSelection.Length())
  73. issuesSelection.Each(func(_ int, selection *goquery.Selection) {
  74. issue := getIssue(t, repo.ID, selection)
  75. assert.Equal(t, user.ID, issue.PosterID)
  76. })
  77. }
  78. func TestViewIssuesKeyword(t *testing.T) {
  79. defer tests.PrepareTestEnv(t)()
  80. repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
  81. issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{
  82. RepoID: repo.ID,
  83. Index: 1,
  84. })
  85. issues.UpdateIssueIndexer(t.Context(), issue.ID)
  86. time.Sleep(time.Second * 1)
  87. const keyword = "first"
  88. req := NewRequestf(t, "GET", "%s/issues?q=%s", repo.Link(), keyword)
  89. resp := MakeRequest(t, req, http.StatusOK)
  90. htmlDoc := NewHTMLParser(t, resp.Body)
  91. issuesSelection := getIssuesSelection(t, htmlDoc)
  92. assert.Equal(t, 1, issuesSelection.Length())
  93. issuesSelection.Each(func(_ int, selection *goquery.Selection) {
  94. issue := getIssue(t, repo.ID, selection)
  95. assert.False(t, issue.IsClosed)
  96. assert.False(t, issue.IsPull)
  97. assertMatch(t, issue, keyword)
  98. })
  99. }
  100. func TestNoLoginViewIssue(t *testing.T) {
  101. defer tests.PrepareTestEnv(t)()
  102. req := NewRequest(t, "GET", "/user2/repo1/issues/1")
  103. MakeRequest(t, req, http.StatusOK)
  104. }
  105. func testNewIssue(t *testing.T, session *TestSession, user, repo, title, content string) string {
  106. req := NewRequest(t, "GET", path.Join(user, repo, "issues", "new"))
  107. resp := session.MakeRequest(t, req, http.StatusOK)
  108. htmlDoc := NewHTMLParser(t, resp.Body)
  109. link, exists := htmlDoc.doc.Find("form.ui.form").Attr("action")
  110. assert.True(t, exists, "The template has changed")
  111. req = NewRequestWithValues(t, "POST", link, map[string]string{
  112. "_csrf": htmlDoc.GetCSRF(),
  113. "title": title,
  114. "content": content,
  115. })
  116. resp = session.MakeRequest(t, req, http.StatusOK)
  117. issueURL := test.RedirectURL(resp)
  118. req = NewRequest(t, "GET", issueURL)
  119. resp = session.MakeRequest(t, req, http.StatusOK)
  120. htmlDoc = NewHTMLParser(t, resp.Body)
  121. val := htmlDoc.doc.Find("#issue-title-display").Text()
  122. assert.Contains(t, val, title)
  123. val = htmlDoc.doc.Find(".comment .render-content p").First().Text()
  124. assert.Equal(t, content, val)
  125. return issueURL
  126. }
  127. func testIssueDelete(t *testing.T, session *TestSession, issueURL string) {
  128. req := NewRequestWithValues(t, "POST", path.Join(issueURL, "delete"), map[string]string{
  129. "_csrf": GetUserCSRFToken(t, session),
  130. })
  131. session.MakeRequest(t, req, http.StatusSeeOther)
  132. }
  133. func testIssueAssign(t *testing.T, session *TestSession, repoLink string, issueID, assigneeID int64) {
  134. req := NewRequestWithValues(t, "POST", fmt.Sprintf(repoLink+"/issues/assignee?issue_ids=%d", issueID), map[string]string{
  135. "_csrf": GetUserCSRFToken(t, session),
  136. "id": strconv.FormatInt(assigneeID, 10),
  137. "action": "", // empty action means assign
  138. })
  139. session.MakeRequest(t, req, http.StatusOK)
  140. }
  141. func testIssueAddComment(t *testing.T, session *TestSession, issueURL, content, status string) int64 {
  142. req := NewRequest(t, "GET", issueURL)
  143. resp := session.MakeRequest(t, req, http.StatusOK)
  144. htmlDoc := NewHTMLParser(t, resp.Body)
  145. link, exists := htmlDoc.doc.Find("#comment-form").Attr("action")
  146. assert.True(t, exists, "The template has changed")
  147. commentCount := htmlDoc.doc.Find(".comment-list .comment .render-content").Length()
  148. req = NewRequestWithValues(t, "POST", link, map[string]string{
  149. "_csrf": htmlDoc.GetCSRF(),
  150. "content": content,
  151. "status": status,
  152. })
  153. resp = session.MakeRequest(t, req, http.StatusOK)
  154. req = NewRequest(t, "GET", test.RedirectURL(resp))
  155. resp = session.MakeRequest(t, req, http.StatusOK)
  156. htmlDoc = NewHTMLParser(t, resp.Body)
  157. val := strings.TrimSpace(htmlDoc.doc.Find(".comment-list .comment .render-content").Eq(commentCount).Text())
  158. assert.Equal(t, content, val)
  159. idAttr, has := htmlDoc.doc.Find(".comment-list .comment").Eq(commentCount).Attr("id")
  160. idStr := idAttr[strings.LastIndexByte(idAttr, '-')+1:]
  161. assert.True(t, has)
  162. id, err := strconv.Atoi(idStr)
  163. assert.NoError(t, err)
  164. return int64(id)
  165. }
  166. func testIssueChangeMilestone(t *testing.T, session *TestSession, repoLink string, issueID, milestoneID int64) {
  167. req := NewRequestWithValues(t, "POST", fmt.Sprintf(repoLink+"/issues/milestone?issue_ids=%d", issueID), map[string]string{
  168. "_csrf": GetUserCSRFToken(t, session),
  169. "id": strconv.FormatInt(milestoneID, 10),
  170. })
  171. resp := session.MakeRequest(t, req, http.StatusOK)
  172. assert.Equal(t, `{"ok":true}`, strings.TrimSpace(resp.Body.String()))
  173. }
  174. func TestNewIssue(t *testing.T) {
  175. defer tests.PrepareTestEnv(t)()
  176. session := loginUser(t, "user2")
  177. testNewIssue(t, session, "user2", "repo1", "Title", "Description")
  178. }
  179. func TestEditIssue(t *testing.T) {
  180. defer tests.PrepareTestEnv(t)()
  181. session := loginUser(t, "user2")
  182. issueURL := testNewIssue(t, session, "user2", "repo1", "Title", "Description")
  183. req := NewRequestWithValues(t, "POST", issueURL+"/content", map[string]string{
  184. "_csrf": GetUserCSRFToken(t, session),
  185. "content": "modified content",
  186. "context": fmt.Sprintf("/%s/%s", "user2", "repo1"),
  187. })
  188. session.MakeRequest(t, req, http.StatusOK)
  189. req = NewRequestWithValues(t, "POST", issueURL+"/content", map[string]string{
  190. "_csrf": GetUserCSRFToken(t, session),
  191. "content": "modified content",
  192. "context": fmt.Sprintf("/%s/%s", "user2", "repo1"),
  193. })
  194. session.MakeRequest(t, req, http.StatusBadRequest)
  195. req = NewRequestWithValues(t, "POST", issueURL+"/content", map[string]string{
  196. "_csrf": GetUserCSRFToken(t, session),
  197. "content": "modified content",
  198. "content_version": "1",
  199. "context": fmt.Sprintf("/%s/%s", "user2", "repo1"),
  200. })
  201. session.MakeRequest(t, req, http.StatusOK)
  202. }
  203. func TestIssueCommentClose(t *testing.T) {
  204. defer tests.PrepareTestEnv(t)()
  205. session := loginUser(t, "user2")
  206. issueURL := testNewIssue(t, session, "user2", "repo1", "Title", "Description")
  207. testIssueAddComment(t, session, issueURL, "Test comment 1", "")
  208. testIssueAddComment(t, session, issueURL, "Test comment 2", "")
  209. testIssueAddComment(t, session, issueURL, "Test comment 3", "close")
  210. // Validate that issue content has not been updated
  211. req := NewRequest(t, "GET", issueURL)
  212. resp := session.MakeRequest(t, req, http.StatusOK)
  213. htmlDoc := NewHTMLParser(t, resp.Body)
  214. val := htmlDoc.doc.Find(".comment-list .comment .render-content p").First().Text()
  215. assert.Equal(t, "Description", val)
  216. }
  217. func TestIssueCommentDelete(t *testing.T) {
  218. defer tests.PrepareTestEnv(t)()
  219. session := loginUser(t, "user2")
  220. issueURL := testNewIssue(t, session, "user2", "repo1", "Title", "Description")
  221. comment1 := "Test comment 1"
  222. commentID := testIssueAddComment(t, session, issueURL, comment1, "")
  223. comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: commentID})
  224. assert.Equal(t, comment1, comment.Content)
  225. // Using the ID of a comment that does not belong to the repository must fail
  226. req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d/delete", "user5", "repo4", commentID), map[string]string{
  227. "_csrf": GetUserCSRFToken(t, session),
  228. })
  229. session.MakeRequest(t, req, http.StatusNotFound)
  230. req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d/delete", "user2", "repo1", commentID), map[string]string{
  231. "_csrf": GetUserCSRFToken(t, session),
  232. })
  233. session.MakeRequest(t, req, http.StatusOK)
  234. unittest.AssertNotExistsBean(t, &issues_model.Comment{ID: commentID})
  235. }
  236. func TestIssueCommentUpdate(t *testing.T) {
  237. defer tests.PrepareTestEnv(t)()
  238. session := loginUser(t, "user2")
  239. issueURL := testNewIssue(t, session, "user2", "repo1", "Title", "Description")
  240. comment1 := "Test comment 1"
  241. commentID := testIssueAddComment(t, session, issueURL, comment1, "")
  242. comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: commentID})
  243. assert.Equal(t, comment1, comment.Content)
  244. modifiedContent := comment.Content + "MODIFIED"
  245. // Using the ID of a comment that does not belong to the repository must fail
  246. req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d", "user5", "repo4", commentID), map[string]string{
  247. "_csrf": GetUserCSRFToken(t, session),
  248. "content": modifiedContent,
  249. })
  250. session.MakeRequest(t, req, http.StatusNotFound)
  251. req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d", "user2", "repo1", commentID), map[string]string{
  252. "_csrf": GetUserCSRFToken(t, session),
  253. "content": modifiedContent,
  254. })
  255. session.MakeRequest(t, req, http.StatusOK)
  256. comment = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: commentID})
  257. assert.Equal(t, modifiedContent, comment.Content)
  258. }
  259. func TestIssueCommentUpdateSimultaneously(t *testing.T) {
  260. defer tests.PrepareTestEnv(t)()
  261. session := loginUser(t, "user2")
  262. issueURL := testNewIssue(t, session, "user2", "repo1", "Title", "Description")
  263. comment1 := "Test comment 1"
  264. commentID := testIssueAddComment(t, session, issueURL, comment1, "")
  265. comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: commentID})
  266. assert.Equal(t, comment1, comment.Content)
  267. modifiedContent := comment.Content + "MODIFIED"
  268. req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d", "user2", "repo1", commentID), map[string]string{
  269. "_csrf": GetUserCSRFToken(t, session),
  270. "content": modifiedContent,
  271. })
  272. session.MakeRequest(t, req, http.StatusOK)
  273. modifiedContent = comment.Content + "2"
  274. req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d", "user2", "repo1", commentID), map[string]string{
  275. "_csrf": GetUserCSRFToken(t, session),
  276. "content": modifiedContent,
  277. })
  278. session.MakeRequest(t, req, http.StatusBadRequest)
  279. req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d", "user2", "repo1", commentID), map[string]string{
  280. "_csrf": GetUserCSRFToken(t, session),
  281. "content": modifiedContent,
  282. "content_version": "1",
  283. })
  284. session.MakeRequest(t, req, http.StatusOK)
  285. comment = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: commentID})
  286. assert.Equal(t, modifiedContent, comment.Content)
  287. assert.Equal(t, 2, comment.ContentVersion)
  288. }
  289. func TestIssueReaction(t *testing.T) {
  290. defer tests.PrepareTestEnv(t)()
  291. session := loginUser(t, "user2")
  292. issueURL := testNewIssue(t, session, "user2", "repo1", "Title", "Description")
  293. req := NewRequest(t, "GET", issueURL)
  294. resp := session.MakeRequest(t, req, http.StatusOK)
  295. htmlDoc := NewHTMLParser(t, resp.Body)
  296. req = NewRequestWithValues(t, "POST", path.Join(issueURL, "/reactions/react"), map[string]string{
  297. "_csrf": htmlDoc.GetCSRF(),
  298. "content": "8ball",
  299. })
  300. session.MakeRequest(t, req, http.StatusInternalServerError)
  301. req = NewRequestWithValues(t, "POST", path.Join(issueURL, "/reactions/react"), map[string]string{
  302. "_csrf": htmlDoc.GetCSRF(),
  303. "content": "eyes",
  304. })
  305. session.MakeRequest(t, req, http.StatusOK)
  306. req = NewRequestWithValues(t, "POST", path.Join(issueURL, "/reactions/unreact"), map[string]string{
  307. "_csrf": htmlDoc.GetCSRF(),
  308. "content": "eyes",
  309. })
  310. session.MakeRequest(t, req, http.StatusOK)
  311. }
  312. func TestIssueCrossReference(t *testing.T) {
  313. defer tests.PrepareTestEnv(t)()
  314. // Issue that will be referenced
  315. _, issueBase := testIssueWithBean(t, "user2", 1, "Title", "Description")
  316. // Ref from issue title
  317. issueRefURL, issueRef := testIssueWithBean(t, "user2", 1, fmt.Sprintf("Title ref #%d", issueBase.Index), "Description")
  318. unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{
  319. IssueID: issueBase.ID,
  320. RefRepoID: 1,
  321. RefIssueID: issueRef.ID,
  322. RefCommentID: 0,
  323. RefIsPull: false,
  324. RefAction: references.XRefActionNone,
  325. })
  326. // Edit title, neuter ref
  327. testIssueChangeInfo(t, "user2", issueRefURL, "title", "Title no ref")
  328. unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{
  329. IssueID: issueBase.ID,
  330. RefRepoID: 1,
  331. RefIssueID: issueRef.ID,
  332. RefCommentID: 0,
  333. RefIsPull: false,
  334. RefAction: references.XRefActionNeutered,
  335. })
  336. // Ref from issue content
  337. issueRefURL, issueRef = testIssueWithBean(t, "user2", 1, "TitleXRef", fmt.Sprintf("Description ref #%d", issueBase.Index))
  338. unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{
  339. IssueID: issueBase.ID,
  340. RefRepoID: 1,
  341. RefIssueID: issueRef.ID,
  342. RefCommentID: 0,
  343. RefIsPull: false,
  344. RefAction: references.XRefActionNone,
  345. })
  346. // Edit content, neuter ref
  347. testIssueChangeInfo(t, "user2", issueRefURL, "content", "Description no ref")
  348. unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{
  349. IssueID: issueBase.ID,
  350. RefRepoID: 1,
  351. RefIssueID: issueRef.ID,
  352. RefCommentID: 0,
  353. RefIsPull: false,
  354. RefAction: references.XRefActionNeutered,
  355. })
  356. // Ref from a comment
  357. session := loginUser(t, "user2")
  358. commentID := testIssueAddComment(t, session, issueRefURL, fmt.Sprintf("Adding ref from comment #%d", issueBase.Index), "")
  359. comment := &issues_model.Comment{
  360. IssueID: issueBase.ID,
  361. RefRepoID: 1,
  362. RefIssueID: issueRef.ID,
  363. RefCommentID: commentID,
  364. RefIsPull: false,
  365. RefAction: references.XRefActionNone,
  366. }
  367. unittest.AssertExistsAndLoadBean(t, comment)
  368. // Ref from a different repository
  369. _, issueRef = testIssueWithBean(t, "user12", 10, "TitleXRef", fmt.Sprintf("Description ref user2/repo1#%d", issueBase.Index))
  370. unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{
  371. IssueID: issueBase.ID,
  372. RefRepoID: 10,
  373. RefIssueID: issueRef.ID,
  374. RefCommentID: 0,
  375. RefIsPull: false,
  376. RefAction: references.XRefActionNone,
  377. })
  378. }
  379. func testIssueWithBean(t *testing.T, user string, repoID int64, title, content string) (string, *issues_model.Issue) {
  380. session := loginUser(t, user)
  381. issueURL := testNewIssue(t, session, user, fmt.Sprintf("repo%d", repoID), title, content)
  382. indexStr := issueURL[strings.LastIndexByte(issueURL, '/')+1:]
  383. index, err := strconv.Atoi(indexStr)
  384. assert.NoError(t, err, "Invalid issue href: %s", issueURL)
  385. issue := &issues_model.Issue{RepoID: repoID, Index: int64(index)}
  386. unittest.AssertExistsAndLoadBean(t, issue)
  387. return issueURL, issue
  388. }
  389. func testIssueChangeInfo(t *testing.T, user, issueURL, info, value string) {
  390. session := loginUser(t, user)
  391. req := NewRequest(t, "GET", issueURL)
  392. resp := session.MakeRequest(t, req, http.StatusOK)
  393. htmlDoc := NewHTMLParser(t, resp.Body)
  394. req = NewRequestWithValues(t, "POST", path.Join(issueURL, info), map[string]string{
  395. "_csrf": htmlDoc.GetCSRF(),
  396. info: value,
  397. })
  398. _ = session.MakeRequest(t, req, http.StatusOK)
  399. }
  400. func TestIssueRedirect(t *testing.T) {
  401. defer tests.PrepareTestEnv(t)()
  402. session := loginUser(t, "user2")
  403. // Test external tracker where style not set (shall default numeric)
  404. req := NewRequest(t, "GET", "/org26/repo_external_tracker/issues/1")
  405. resp := session.MakeRequest(t, req, http.StatusSeeOther)
  406. assert.Equal(t, "https://tracker.com/org26/repo_external_tracker/issues/1", test.RedirectURL(resp))
  407. // Test external tracker with numeric style
  408. req = NewRequest(t, "GET", "/org26/repo_external_tracker_numeric/issues/1")
  409. resp = session.MakeRequest(t, req, http.StatusSeeOther)
  410. assert.Equal(t, "https://tracker.com/org26/repo_external_tracker_numeric/issues/1", test.RedirectURL(resp))
  411. // Test external tracker with alphanumeric style (for a pull request)
  412. req = NewRequest(t, "GET", "/org26/repo_external_tracker_alpha/issues/1")
  413. resp = session.MakeRequest(t, req, http.StatusSeeOther)
  414. assert.Equal(t, "/org26/repo_external_tracker_alpha/pulls/1", test.RedirectURL(resp))
  415. // test to check that the PR redirection works if the issue unit is disabled
  416. // repo1 is a normal repository with issue unit enabled, visit issue 2(which is a pull request)
  417. // will redirect to pulls
  418. req = NewRequest(t, "GET", "/user2/repo1/issues/2")
  419. resp = session.MakeRequest(t, req, http.StatusSeeOther)
  420. assert.Equal(t, "/user2/repo1/pulls/2", test.RedirectURL(resp))
  421. repoUnit := unittest.AssertExistsAndLoadBean(t, &repo_model.RepoUnit{RepoID: 1, Type: unit.TypeIssues})
  422. // disable issue unit, it will be reset
  423. _, err := db.DeleteByID[repo_model.RepoUnit](t.Context(), repoUnit.ID)
  424. assert.NoError(t, err)
  425. // even if the issue unit is disabled, visiting an issue which is a pull request
  426. // will still redirect to pull request
  427. req = NewRequest(t, "GET", "/user2/repo1/issues/2")
  428. resp = session.MakeRequest(t, req, http.StatusSeeOther)
  429. assert.Equal(t, "/user2/repo1/pulls/2", test.RedirectURL(resp))
  430. }
  431. func TestSearchIssues(t *testing.T) {
  432. defer tests.PrepareTestEnv(t)()
  433. session := loginUser(t, "user2")
  434. expectedIssueCount := min(20, setting.UI.IssuePagingNum) // 20 is from the fixtures
  435. link, _ := url.Parse("/issues/search")
  436. req := NewRequest(t, "GET", link.String())
  437. resp := session.MakeRequest(t, req, http.StatusOK)
  438. var apiIssues []*api.Issue
  439. DecodeJSON(t, resp, &apiIssues)
  440. assert.Len(t, apiIssues, expectedIssueCount)
  441. since := "2000-01-01T00:50:01+00:00" // 946687801
  442. before := time.Unix(999307200, 0).Format(time.RFC3339)
  443. query := url.Values{}
  444. query.Add("since", since)
  445. query.Add("before", before)
  446. link.RawQuery = query.Encode()
  447. req = NewRequest(t, "GET", link.String())
  448. resp = session.MakeRequest(t, req, http.StatusOK)
  449. DecodeJSON(t, resp, &apiIssues)
  450. assert.Len(t, apiIssues, 11)
  451. query.Del("since")
  452. query.Del("before")
  453. query.Add("state", "closed")
  454. link.RawQuery = query.Encode()
  455. req = NewRequest(t, "GET", link.String())
  456. resp = session.MakeRequest(t, req, http.StatusOK)
  457. DecodeJSON(t, resp, &apiIssues)
  458. assert.Len(t, apiIssues, 2)
  459. query.Set("state", "all")
  460. link.RawQuery = query.Encode()
  461. req = NewRequest(t, "GET", link.String())
  462. resp = session.MakeRequest(t, req, http.StatusOK)
  463. DecodeJSON(t, resp, &apiIssues)
  464. assert.Equal(t, "22", resp.Header().Get("X-Total-Count"))
  465. assert.Len(t, apiIssues, 20)
  466. query.Add("limit", "5")
  467. link.RawQuery = query.Encode()
  468. req = NewRequest(t, "GET", link.String())
  469. resp = session.MakeRequest(t, req, http.StatusOK)
  470. DecodeJSON(t, resp, &apiIssues)
  471. assert.Equal(t, "22", resp.Header().Get("X-Total-Count"))
  472. assert.Len(t, apiIssues, 5)
  473. query = url.Values{"assigned": {"true"}, "state": {"all"}}
  474. link.RawQuery = query.Encode()
  475. req = NewRequest(t, "GET", link.String())
  476. resp = session.MakeRequest(t, req, http.StatusOK)
  477. DecodeJSON(t, resp, &apiIssues)
  478. assert.Len(t, apiIssues, 2)
  479. query = url.Values{"milestones": {"milestone1"}, "state": {"all"}}
  480. link.RawQuery = query.Encode()
  481. req = NewRequest(t, "GET", link.String())
  482. resp = session.MakeRequest(t, req, http.StatusOK)
  483. DecodeJSON(t, resp, &apiIssues)
  484. assert.Len(t, apiIssues, 1)
  485. query = url.Values{"milestones": {"milestone1,milestone3"}, "state": {"all"}}
  486. link.RawQuery = query.Encode()
  487. req = NewRequest(t, "GET", link.String())
  488. resp = session.MakeRequest(t, req, http.StatusOK)
  489. DecodeJSON(t, resp, &apiIssues)
  490. assert.Len(t, apiIssues, 2)
  491. query = url.Values{"owner": {"user2"}} // user
  492. link.RawQuery = query.Encode()
  493. req = NewRequest(t, "GET", link.String())
  494. resp = session.MakeRequest(t, req, http.StatusOK)
  495. DecodeJSON(t, resp, &apiIssues)
  496. assert.Len(t, apiIssues, 8)
  497. query = url.Values{"owner": {"org3"}} // organization
  498. link.RawQuery = query.Encode()
  499. req = NewRequest(t, "GET", link.String())
  500. resp = session.MakeRequest(t, req, http.StatusOK)
  501. DecodeJSON(t, resp, &apiIssues)
  502. assert.Len(t, apiIssues, 5)
  503. query = url.Values{"owner": {"org3"}, "team": {"team1"}} // organization + team
  504. link.RawQuery = query.Encode()
  505. req = NewRequest(t, "GET", link.String())
  506. resp = session.MakeRequest(t, req, http.StatusOK)
  507. DecodeJSON(t, resp, &apiIssues)
  508. assert.Len(t, apiIssues, 2)
  509. }
  510. func TestSearchIssuesWithLabels(t *testing.T) {
  511. defer tests.PrepareTestEnv(t)()
  512. expectedIssueCount := min(20, setting.UI.IssuePagingNum) // 20 is from the fixtures
  513. session := loginUser(t, "user1")
  514. link, _ := url.Parse("/issues/search")
  515. query := url.Values{}
  516. var apiIssues []*api.Issue
  517. link.RawQuery = query.Encode()
  518. req := NewRequest(t, "GET", link.String())
  519. resp := session.MakeRequest(t, req, http.StatusOK)
  520. DecodeJSON(t, resp, &apiIssues)
  521. assert.Len(t, apiIssues, expectedIssueCount)
  522. query.Add("labels", "label1")
  523. link.RawQuery = query.Encode()
  524. req = NewRequest(t, "GET", link.String())
  525. resp = session.MakeRequest(t, req, http.StatusOK)
  526. DecodeJSON(t, resp, &apiIssues)
  527. assert.Len(t, apiIssues, 2)
  528. // multiple labels
  529. query.Set("labels", "label1,label2")
  530. link.RawQuery = query.Encode()
  531. req = NewRequest(t, "GET", link.String())
  532. resp = session.MakeRequest(t, req, http.StatusOK)
  533. DecodeJSON(t, resp, &apiIssues)
  534. assert.Len(t, apiIssues, 2)
  535. // an org label
  536. query.Set("labels", "orglabel4")
  537. link.RawQuery = query.Encode()
  538. req = NewRequest(t, "GET", link.String())
  539. resp = session.MakeRequest(t, req, http.StatusOK)
  540. DecodeJSON(t, resp, &apiIssues)
  541. assert.Len(t, apiIssues, 1)
  542. // org and repo label
  543. query.Set("labels", "label2,orglabel4")
  544. query.Add("state", "all")
  545. link.RawQuery = query.Encode()
  546. req = NewRequest(t, "GET", link.String())
  547. resp = session.MakeRequest(t, req, http.StatusOK)
  548. DecodeJSON(t, resp, &apiIssues)
  549. assert.Len(t, apiIssues, 2)
  550. // org and repo label which share the same issue
  551. query.Set("labels", "label1,orglabel4")
  552. link.RawQuery = query.Encode()
  553. req = NewRequest(t, "GET", link.String())
  554. resp = session.MakeRequest(t, req, http.StatusOK)
  555. DecodeJSON(t, resp, &apiIssues)
  556. assert.Len(t, apiIssues, 2)
  557. }
  558. func TestGetIssueInfo(t *testing.T) {
  559. defer tests.PrepareTestEnv(t)()
  560. issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 10})
  561. repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
  562. owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
  563. assert.NoError(t, issue.LoadAttributes(t.Context()))
  564. assert.Equal(t, int64(1019307200), int64(issue.DeadlineUnix))
  565. assert.Equal(t, api.StateOpen, issue.State())
  566. session := loginUser(t, owner.Name)
  567. urlStr := fmt.Sprintf("/%s/%s/issues/%d/info", owner.Name, repo.Name, issue.Index)
  568. req := NewRequest(t, "GET", urlStr)
  569. resp := session.MakeRequest(t, req, http.StatusOK)
  570. var respStruct struct {
  571. ConvertedIssue api.Issue
  572. RenderedLabels template.HTML
  573. }
  574. DecodeJSON(t, resp, &respStruct)
  575. assert.Equal(t, issue.ID, respStruct.ConvertedIssue.ID)
  576. assert.Contains(t, string(respStruct.RenderedLabels), `"labels-list"`)
  577. }
  578. func TestUpdateIssueDeadline(t *testing.T) {
  579. defer tests.PrepareTestEnv(t)()
  580. issueBefore := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 10})
  581. repoBefore := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issueBefore.RepoID})
  582. owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repoBefore.OwnerID})
  583. assert.NoError(t, issueBefore.LoadAttributes(t.Context()))
  584. assert.Equal(t, "2002-04-20", issueBefore.DeadlineUnix.FormatDate())
  585. assert.Equal(t, api.StateOpen, issueBefore.State())
  586. session := loginUser(t, owner.Name)
  587. urlStr := fmt.Sprintf("%s/%s/issues/%d/deadline?_csrf=%s", owner.Name, repoBefore.Name, issueBefore.Index, GetUserCSRFToken(t, session))
  588. req := NewRequestWithValues(t, "POST", urlStr, map[string]string{"deadline": "2022-04-06"})
  589. session.MakeRequest(t, req, http.StatusOK)
  590. issueAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 10})
  591. assert.Equal(t, "2022-04-06", issueAfter.DeadlineUnix.FormatDate())
  592. req = NewRequestWithValues(t, "POST", urlStr, map[string]string{"deadline": ""})
  593. session.MakeRequest(t, req, http.StatusOK)
  594. issueAfter = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 10})
  595. assert.True(t, issueAfter.DeadlineUnix.IsZero())
  596. }
  597. func TestIssueReferenceURL(t *testing.T) {
  598. defer tests.PrepareTestEnv(t)()
  599. session := loginUser(t, "user2")
  600. issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
  601. repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
  602. req := NewRequest(t, "GET", fmt.Sprintf("%s/issues/%d", repo.FullName(), issue.Index))
  603. resp := session.MakeRequest(t, req, http.StatusOK)
  604. htmlDoc := NewHTMLParser(t, resp.Body)
  605. // the "reference" uses relative URLs, then JS code will convert them to absolute URLs for current origin, in case users are using multiple domains
  606. ref, _ := htmlDoc.Find(`.timeline-item.comment.first .reference-issue`).Attr("data-reference")
  607. assert.Equal(t, "/user2/repo1/issues/1#issue-1", ref)
  608. ref, _ = htmlDoc.Find(`.timeline-item.comment:not(.first) .reference-issue`).Attr("data-reference")
  609. assert.Equal(t, "/user2/repo1/issues/1#issuecomment-2", ref)
  610. }