You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

issue_test.go 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  1. // Copyright 2017 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package issues_test
  4. import (
  5. "context"
  6. "fmt"
  7. "sort"
  8. "sync"
  9. "testing"
  10. "time"
  11. "code.gitea.io/gitea/models/db"
  12. issues_model "code.gitea.io/gitea/models/issues"
  13. "code.gitea.io/gitea/models/organization"
  14. repo_model "code.gitea.io/gitea/models/repo"
  15. "code.gitea.io/gitea/models/unittest"
  16. user_model "code.gitea.io/gitea/models/user"
  17. "github.com/stretchr/testify/assert"
  18. "xorm.io/builder"
  19. )
  20. func TestIssue_ReplaceLabels(t *testing.T) {
  21. assert.NoError(t, unittest.PrepareTestDatabase())
  22. testSuccess := func(issueID int64, labelIDs []int64) {
  23. issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issueID})
  24. repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
  25. doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
  26. labels := make([]*issues_model.Label, len(labelIDs))
  27. for i, labelID := range labelIDs {
  28. labels[i] = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: labelID, RepoID: repo.ID})
  29. }
  30. assert.NoError(t, issues_model.ReplaceIssueLabels(issue, labels, doer))
  31. unittest.AssertCount(t, &issues_model.IssueLabel{IssueID: issueID}, len(labelIDs))
  32. for _, labelID := range labelIDs {
  33. unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issueID, LabelID: labelID})
  34. }
  35. }
  36. testSuccess(1, []int64{2})
  37. testSuccess(1, []int64{1, 2})
  38. testSuccess(1, []int64{})
  39. }
  40. func Test_GetIssueIDsByRepoID(t *testing.T) {
  41. assert.NoError(t, unittest.PrepareTestDatabase())
  42. ids, err := issues_model.GetIssueIDsByRepoID(db.DefaultContext, 1)
  43. assert.NoError(t, err)
  44. assert.Len(t, ids, 5)
  45. }
  46. func TestIssueAPIURL(t *testing.T) {
  47. assert.NoError(t, unittest.PrepareTestDatabase())
  48. issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
  49. err := issue.LoadAttributes(db.DefaultContext)
  50. assert.NoError(t, err)
  51. assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/issues/1", issue.APIURL())
  52. }
  53. func TestGetIssuesByIDs(t *testing.T) {
  54. assert.NoError(t, unittest.PrepareTestDatabase())
  55. testSuccess := func(expectedIssueIDs, nonExistentIssueIDs []int64) {
  56. issues, err := issues_model.GetIssuesByIDs(db.DefaultContext, append(expectedIssueIDs, nonExistentIssueIDs...))
  57. assert.NoError(t, err)
  58. actualIssueIDs := make([]int64, len(issues))
  59. for i, issue := range issues {
  60. actualIssueIDs[i] = issue.ID
  61. }
  62. assert.Equal(t, expectedIssueIDs, actualIssueIDs)
  63. }
  64. testSuccess([]int64{1, 2, 3}, []int64{})
  65. testSuccess([]int64{1, 2, 3}, []int64{unittest.NonexistentID})
  66. }
  67. func TestGetParticipantIDsByIssue(t *testing.T) {
  68. assert.NoError(t, unittest.PrepareTestDatabase())
  69. checkParticipants := func(issueID int64, userIDs []int) {
  70. issue, err := issues_model.GetIssueByID(db.DefaultContext, issueID)
  71. assert.NoError(t, err)
  72. participants, err := issue.GetParticipantIDsByIssue(db.DefaultContext)
  73. if assert.NoError(t, err) {
  74. participantsIDs := make([]int, len(participants))
  75. for i, uid := range participants {
  76. participantsIDs[i] = int(uid)
  77. }
  78. sort.Ints(participantsIDs)
  79. sort.Ints(userIDs)
  80. assert.Equal(t, userIDs, participantsIDs)
  81. }
  82. }
  83. // User 1 is issue1 poster (see fixtures/issue.yml)
  84. // User 2 only labeled issue1 (see fixtures/comment.yml)
  85. // Users 3 and 5 made actual comments (see fixtures/comment.yml)
  86. // User 3 is inactive, thus not active participant
  87. checkParticipants(1, []int{1, 5})
  88. }
  89. func TestIssue_ClearLabels(t *testing.T) {
  90. tests := []struct {
  91. issueID int64
  92. doerID int64
  93. }{
  94. {1, 2}, // non-pull-request, has labels
  95. {2, 2}, // pull-request, has labels
  96. {3, 2}, // pull-request, has no labels
  97. }
  98. for _, test := range tests {
  99. assert.NoError(t, unittest.PrepareTestDatabase())
  100. issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: test.issueID})
  101. doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: test.doerID})
  102. assert.NoError(t, issues_model.ClearIssueLabels(issue, doer))
  103. unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{IssueID: test.issueID})
  104. }
  105. }
  106. func TestUpdateIssueCols(t *testing.T) {
  107. assert.NoError(t, unittest.PrepareTestDatabase())
  108. issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{})
  109. const newTitle = "New Title for unit test"
  110. issue.Title = newTitle
  111. prevContent := issue.Content
  112. issue.Content = "This should have no effect"
  113. now := time.Now().Unix()
  114. assert.NoError(t, issues_model.UpdateIssueCols(db.DefaultContext, issue, "name"))
  115. then := time.Now().Unix()
  116. updatedIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issue.ID})
  117. assert.EqualValues(t, newTitle, updatedIssue.Title)
  118. assert.EqualValues(t, prevContent, updatedIssue.Content)
  119. unittest.AssertInt64InRange(t, now, then, int64(updatedIssue.UpdatedUnix))
  120. }
  121. func TestIssues(t *testing.T) {
  122. assert.NoError(t, unittest.PrepareTestDatabase())
  123. for _, test := range []struct {
  124. Opts issues_model.IssuesOptions
  125. ExpectedIssueIDs []int64
  126. }{
  127. {
  128. issues_model.IssuesOptions{
  129. AssigneeID: 1,
  130. SortType: "oldest",
  131. },
  132. []int64{1, 6},
  133. },
  134. {
  135. issues_model.IssuesOptions{
  136. RepoCond: builder.In("repo_id", 1, 3),
  137. SortType: "oldest",
  138. ListOptions: db.ListOptions{
  139. Page: 1,
  140. PageSize: 4,
  141. },
  142. },
  143. []int64{1, 2, 3, 5},
  144. },
  145. {
  146. issues_model.IssuesOptions{
  147. LabelIDs: []int64{1},
  148. ListOptions: db.ListOptions{
  149. Page: 1,
  150. PageSize: 4,
  151. },
  152. },
  153. []int64{2, 1},
  154. },
  155. {
  156. issues_model.IssuesOptions{
  157. LabelIDs: []int64{1, 2},
  158. ListOptions: db.ListOptions{
  159. Page: 1,
  160. PageSize: 4,
  161. },
  162. },
  163. []int64{}, // issues with **both** label 1 and 2, none of these issues matches, TODO: add more tests
  164. },
  165. } {
  166. issues, err := issues_model.Issues(db.DefaultContext, &test.Opts)
  167. assert.NoError(t, err)
  168. if assert.Len(t, issues, len(test.ExpectedIssueIDs)) {
  169. for i, issue := range issues {
  170. assert.EqualValues(t, test.ExpectedIssueIDs[i], issue.ID)
  171. }
  172. }
  173. }
  174. }
  175. func TestGetUserIssueStats(t *testing.T) {
  176. assert.NoError(t, unittest.PrepareTestDatabase())
  177. for _, test := range []struct {
  178. Opts issues_model.UserIssueStatsOptions
  179. ExpectedIssueStats issues_model.IssueStats
  180. }{
  181. {
  182. issues_model.UserIssueStatsOptions{
  183. UserID: 1,
  184. RepoIDs: []int64{1},
  185. FilterMode: issues_model.FilterModeAll,
  186. },
  187. issues_model.IssueStats{
  188. YourRepositoriesCount: 1, // 6
  189. AssignCount: 1, // 6
  190. CreateCount: 1, // 6
  191. OpenCount: 1, // 6
  192. ClosedCount: 1, // 1
  193. },
  194. },
  195. {
  196. issues_model.UserIssueStatsOptions{
  197. UserID: 1,
  198. RepoIDs: []int64{1},
  199. FilterMode: issues_model.FilterModeAll,
  200. IsClosed: true,
  201. },
  202. issues_model.IssueStats{
  203. YourRepositoriesCount: 1, // 6
  204. AssignCount: 0,
  205. CreateCount: 0,
  206. OpenCount: 1, // 6
  207. ClosedCount: 1, // 1
  208. },
  209. },
  210. {
  211. issues_model.UserIssueStatsOptions{
  212. UserID: 1,
  213. FilterMode: issues_model.FilterModeAssign,
  214. },
  215. issues_model.IssueStats{
  216. YourRepositoriesCount: 1, // 6
  217. AssignCount: 1, // 6
  218. CreateCount: 1, // 6
  219. OpenCount: 1, // 6
  220. ClosedCount: 0,
  221. },
  222. },
  223. {
  224. issues_model.UserIssueStatsOptions{
  225. UserID: 1,
  226. FilterMode: issues_model.FilterModeCreate,
  227. },
  228. issues_model.IssueStats{
  229. YourRepositoriesCount: 1, // 6
  230. AssignCount: 1, // 6
  231. CreateCount: 1, // 6
  232. OpenCount: 1, // 6
  233. ClosedCount: 0,
  234. },
  235. },
  236. {
  237. issues_model.UserIssueStatsOptions{
  238. UserID: 1,
  239. FilterMode: issues_model.FilterModeMention,
  240. },
  241. issues_model.IssueStats{
  242. YourRepositoriesCount: 1, // 6
  243. AssignCount: 1, // 6
  244. CreateCount: 1, // 6
  245. MentionCount: 0,
  246. OpenCount: 0,
  247. ClosedCount: 0,
  248. },
  249. },
  250. {
  251. issues_model.UserIssueStatsOptions{
  252. UserID: 1,
  253. FilterMode: issues_model.FilterModeCreate,
  254. IssueIDs: []int64{1},
  255. },
  256. issues_model.IssueStats{
  257. YourRepositoriesCount: 1, // 1
  258. AssignCount: 1, // 1
  259. CreateCount: 1, // 1
  260. OpenCount: 1, // 1
  261. ClosedCount: 0,
  262. },
  263. },
  264. {
  265. issues_model.UserIssueStatsOptions{
  266. UserID: 2,
  267. Org: unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}),
  268. Team: unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 7}),
  269. FilterMode: issues_model.FilterModeAll,
  270. },
  271. issues_model.IssueStats{
  272. YourRepositoriesCount: 2,
  273. AssignCount: 1,
  274. CreateCount: 1,
  275. OpenCount: 2,
  276. },
  277. },
  278. } {
  279. t.Run(fmt.Sprintf("%#v", test.Opts), func(t *testing.T) {
  280. stats, err := issues_model.GetUserIssueStats(test.Opts)
  281. if !assert.NoError(t, err) {
  282. return
  283. }
  284. assert.Equal(t, test.ExpectedIssueStats, *stats)
  285. })
  286. }
  287. }
  288. func TestIssue_loadTotalTimes(t *testing.T) {
  289. assert.NoError(t, unittest.PrepareTestDatabase())
  290. ms, err := issues_model.GetIssueByID(db.DefaultContext, 2)
  291. assert.NoError(t, err)
  292. assert.NoError(t, ms.LoadTotalTimes(db.DefaultContext))
  293. assert.Equal(t, int64(3682), ms.TotalTrackedTime)
  294. }
  295. func TestIssue_SearchIssueIDsByKeyword(t *testing.T) {
  296. assert.NoError(t, unittest.PrepareTestDatabase())
  297. total, ids, err := issues_model.SearchIssueIDsByKeyword(context.TODO(), "issue2", []int64{1}, 10, 0)
  298. assert.NoError(t, err)
  299. assert.EqualValues(t, 1, total)
  300. assert.EqualValues(t, []int64{2}, ids)
  301. total, ids, err = issues_model.SearchIssueIDsByKeyword(context.TODO(), "first", []int64{1}, 10, 0)
  302. assert.NoError(t, err)
  303. assert.EqualValues(t, 1, total)
  304. assert.EqualValues(t, []int64{1}, ids)
  305. total, ids, err = issues_model.SearchIssueIDsByKeyword(context.TODO(), "for", []int64{1}, 10, 0)
  306. assert.NoError(t, err)
  307. assert.EqualValues(t, 5, total)
  308. assert.ElementsMatch(t, []int64{1, 2, 3, 5, 11}, ids)
  309. // issue1's comment id 2
  310. total, ids, err = issues_model.SearchIssueIDsByKeyword(context.TODO(), "good", []int64{1}, 10, 0)
  311. assert.NoError(t, err)
  312. assert.EqualValues(t, 1, total)
  313. assert.EqualValues(t, []int64{1}, ids)
  314. }
  315. func TestGetRepoIDsForIssuesOptions(t *testing.T) {
  316. assert.NoError(t, unittest.PrepareTestDatabase())
  317. user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
  318. for _, test := range []struct {
  319. Opts issues_model.IssuesOptions
  320. ExpectedRepoIDs []int64
  321. }{
  322. {
  323. issues_model.IssuesOptions{
  324. AssigneeID: 2,
  325. },
  326. []int64{3, 32},
  327. },
  328. {
  329. issues_model.IssuesOptions{
  330. RepoCond: builder.In("repo_id", 1, 2),
  331. },
  332. []int64{1, 2},
  333. },
  334. } {
  335. repoIDs, err := issues_model.GetRepoIDsForIssuesOptions(&test.Opts, user)
  336. assert.NoError(t, err)
  337. if assert.Len(t, repoIDs, len(test.ExpectedRepoIDs)) {
  338. for i, repoID := range repoIDs {
  339. assert.EqualValues(t, test.ExpectedRepoIDs[i], repoID)
  340. }
  341. }
  342. }
  343. }
  344. func testInsertIssue(t *testing.T, title, content string, expectIndex int64) *issues_model.Issue {
  345. var newIssue issues_model.Issue
  346. t.Run(title, func(t *testing.T) {
  347. repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
  348. user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
  349. issue := issues_model.Issue{
  350. RepoID: repo.ID,
  351. PosterID: user.ID,
  352. Poster: user,
  353. Title: title,
  354. Content: content,
  355. }
  356. err := issues_model.NewIssue(repo, &issue, nil, nil)
  357. assert.NoError(t, err)
  358. has, err := db.GetEngine(db.DefaultContext).ID(issue.ID).Get(&newIssue)
  359. assert.NoError(t, err)
  360. assert.True(t, has)
  361. assert.EqualValues(t, issue.Title, newIssue.Title)
  362. assert.EqualValues(t, issue.Content, newIssue.Content)
  363. if expectIndex > 0 {
  364. assert.EqualValues(t, expectIndex, newIssue.Index)
  365. }
  366. })
  367. return &newIssue
  368. }
  369. func TestIssue_InsertIssue(t *testing.T) {
  370. assert.NoError(t, unittest.PrepareTestDatabase())
  371. // there are 5 issues and max index is 5 on repository 1, so this one should 6
  372. issue := testInsertIssue(t, "my issue1", "special issue's comments?", 6)
  373. _, err := db.GetEngine(db.DefaultContext).ID(issue.ID).Delete(new(issues_model.Issue))
  374. assert.NoError(t, err)
  375. issue = testInsertIssue(t, `my issue2, this is my son's love \n \r \ `, "special issue's '' comments?", 7)
  376. _, err = db.GetEngine(db.DefaultContext).ID(issue.ID).Delete(new(issues_model.Issue))
  377. assert.NoError(t, err)
  378. }
  379. func TestIssue_ResolveMentions(t *testing.T) {
  380. assert.NoError(t, unittest.PrepareTestDatabase())
  381. testSuccess := func(owner, repo, doer string, mentions []string, expected []int64) {
  382. o := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: owner})
  383. r := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: o.ID, LowerName: repo})
  384. issue := &issues_model.Issue{RepoID: r.ID}
  385. d := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: doer})
  386. resolved, err := issues_model.ResolveIssueMentionsByVisibility(db.DefaultContext, issue, d, mentions)
  387. assert.NoError(t, err)
  388. ids := make([]int64, len(resolved))
  389. for i, user := range resolved {
  390. ids[i] = user.ID
  391. }
  392. sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
  393. assert.EqualValues(t, expected, ids)
  394. }
  395. // Public repo, existing user
  396. testSuccess("user2", "repo1", "user1", []string{"user5"}, []int64{5})
  397. // Public repo, non-existing user
  398. testSuccess("user2", "repo1", "user1", []string{"nonexisting"}, []int64{})
  399. // Public repo, doer
  400. testSuccess("user2", "repo1", "user1", []string{"user1"}, []int64{})
  401. // Private repo, team member
  402. testSuccess("user17", "big_test_private_4", "user20", []string{"user2"}, []int64{2})
  403. // Private repo, not a team member
  404. testSuccess("user17", "big_test_private_4", "user20", []string{"user5"}, []int64{})
  405. // Private repo, whole team
  406. testSuccess("user17", "big_test_private_4", "user15", []string{"user17/owners"}, []int64{18})
  407. }
  408. func TestResourceIndex(t *testing.T) {
  409. assert.NoError(t, unittest.PrepareTestDatabase())
  410. var wg sync.WaitGroup
  411. for i := 0; i < 100; i++ {
  412. wg.Add(1)
  413. go func(i int) {
  414. testInsertIssue(t, fmt.Sprintf("issue %d", i+1), "my issue", 0)
  415. wg.Done()
  416. }(i)
  417. }
  418. wg.Wait()
  419. }
  420. func TestCorrectIssueStats(t *testing.T) {
  421. assert.NoError(t, unittest.PrepareTestDatabase())
  422. // Because the condition is to have chunked database look-ups,
  423. // We have to more issues than `maxQueryParameters`, we will insert.
  424. // maxQueryParameters + 10 issues into the testDatabase.
  425. // Each new issues will have a constant description "Bugs are nasty"
  426. // Which will be used later on.
  427. issueAmount := issues_model.MaxQueryParameters + 10
  428. var wg sync.WaitGroup
  429. for i := 0; i < issueAmount; i++ {
  430. wg.Add(1)
  431. go func(i int) {
  432. testInsertIssue(t, fmt.Sprintf("Issue %d", i+1), "Bugs are nasty", 0)
  433. wg.Done()
  434. }(i)
  435. }
  436. wg.Wait()
  437. // Now we will get all issueID's that match the "Bugs are nasty" query.
  438. total, ids, err := issues_model.SearchIssueIDsByKeyword(context.TODO(), "Bugs are nasty", []int64{1}, issueAmount, 0)
  439. // Just to be sure.
  440. assert.NoError(t, err)
  441. assert.EqualValues(t, issueAmount, total)
  442. // Now we will call the GetIssueStats with these IDs and if working,
  443. // get the correct stats back.
  444. issueStats, err := issues_model.GetIssueStats(&issues_model.IssueStatsOptions{
  445. RepoID: 1,
  446. IssueIDs: ids,
  447. })
  448. // Now check the values.
  449. assert.NoError(t, err)
  450. assert.EqualValues(t, issueStats.OpenCount, issueAmount)
  451. }
  452. func TestMilestoneList_LoadTotalTrackedTimes(t *testing.T) {
  453. assert.NoError(t, unittest.PrepareTestDatabase())
  454. miles := issues_model.MilestoneList{
  455. unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}),
  456. }
  457. assert.NoError(t, miles.LoadTotalTrackedTimes())
  458. assert.Equal(t, int64(3682), miles[0].TotalTrackedTime)
  459. }
  460. func TestLoadTotalTrackedTime(t *testing.T) {
  461. assert.NoError(t, unittest.PrepareTestDatabase())
  462. milestone := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1})
  463. assert.NoError(t, milestone.LoadTotalTrackedTime())
  464. assert.Equal(t, int64(3682), milestone.TotalTrackedTime)
  465. }
  466. func TestCountIssues(t *testing.T) {
  467. assert.NoError(t, unittest.PrepareTestDatabase())
  468. count, err := issues_model.CountIssues(db.DefaultContext, &issues_model.IssuesOptions{})
  469. assert.NoError(t, err)
  470. assert.EqualValues(t, 17, count)
  471. }