code.gitea.io/gitea@v1.22.3/models/issues/issue_test.go (about)

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