code.gitea.io/gitea@v1.22.3/services/migrations/gitea_uploader_test.go (about)

     1  // Copyright 2019 The Gitea Authors. All rights reserved.
     2  // Copyright 2018 Jonas Franz. All rights reserved.
     3  // SPDX-License-Identifier: MIT
     4  
     5  package migrations
     6  
     7  import (
     8  	"context"
     9  	"fmt"
    10  	"os"
    11  	"path/filepath"
    12  	"strconv"
    13  	"testing"
    14  	"time"
    15  
    16  	"code.gitea.io/gitea/models/db"
    17  	issues_model "code.gitea.io/gitea/models/issues"
    18  	repo_model "code.gitea.io/gitea/models/repo"
    19  	"code.gitea.io/gitea/models/unittest"
    20  	user_model "code.gitea.io/gitea/models/user"
    21  	"code.gitea.io/gitea/modules/git"
    22  	"code.gitea.io/gitea/modules/gitrepo"
    23  	"code.gitea.io/gitea/modules/graceful"
    24  	"code.gitea.io/gitea/modules/log"
    25  	base "code.gitea.io/gitea/modules/migration"
    26  	"code.gitea.io/gitea/modules/optional"
    27  	"code.gitea.io/gitea/modules/structs"
    28  	"code.gitea.io/gitea/modules/test"
    29  
    30  	"github.com/stretchr/testify/assert"
    31  )
    32  
    33  func TestGiteaUploadRepo(t *testing.T) {
    34  	// FIXME: Since no accesskey or user/password will trigger rate limit of github, just skip
    35  	t.Skip()
    36  
    37  	unittest.PrepareTestEnv(t)
    38  
    39  	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
    40  
    41  	var (
    42  		ctx        = context.Background()
    43  		downloader = NewGithubDownloaderV3(ctx, "https://github.com", "", "", "", "go-xorm", "builder")
    44  		repoName   = "builder-" + time.Now().Format("2006-01-02-15-04-05")
    45  		uploader   = NewGiteaLocalUploader(graceful.GetManager().HammerContext(), user, user.Name, repoName)
    46  	)
    47  
    48  	err := migrateRepository(db.DefaultContext, user, downloader, uploader, base.MigrateOptions{
    49  		CloneAddr:    "https://github.com/go-xorm/builder",
    50  		RepoName:     repoName,
    51  		AuthUsername: "",
    52  
    53  		Wiki:         true,
    54  		Issues:       true,
    55  		Milestones:   true,
    56  		Labels:       true,
    57  		Releases:     true,
    58  		Comments:     true,
    59  		PullRequests: true,
    60  		Private:      true,
    61  		Mirror:       false,
    62  	}, nil)
    63  	assert.NoError(t, err)
    64  
    65  	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: user.ID, Name: repoName})
    66  	assert.True(t, repo.HasWiki())
    67  	assert.EqualValues(t, repo_model.RepositoryReady, repo.Status)
    68  
    69  	milestones, err := db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
    70  		RepoID:   repo.ID,
    71  		IsClosed: optional.Some(false),
    72  	})
    73  	assert.NoError(t, err)
    74  	assert.Len(t, milestones, 1)
    75  
    76  	milestones, err = db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
    77  		RepoID:   repo.ID,
    78  		IsClosed: optional.Some(true),
    79  	})
    80  	assert.NoError(t, err)
    81  	assert.Empty(t, milestones)
    82  
    83  	labels, err := issues_model.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{})
    84  	assert.NoError(t, err)
    85  	assert.Len(t, labels, 12)
    86  
    87  	releases, err := db.Find[repo_model.Release](db.DefaultContext, repo_model.FindReleasesOptions{
    88  		ListOptions: db.ListOptions{
    89  			PageSize: 10,
    90  			Page:     0,
    91  		},
    92  		IncludeTags: true,
    93  		RepoID:      repo.ID,
    94  	})
    95  	assert.NoError(t, err)
    96  	assert.Len(t, releases, 8)
    97  
    98  	releases, err = db.Find[repo_model.Release](db.DefaultContext, repo_model.FindReleasesOptions{
    99  		ListOptions: db.ListOptions{
   100  			PageSize: 10,
   101  			Page:     0,
   102  		},
   103  		IncludeTags: false,
   104  		RepoID:      repo.ID,
   105  	})
   106  	assert.NoError(t, err)
   107  	assert.Len(t, releases, 1)
   108  
   109  	issues, err := issues_model.Issues(db.DefaultContext, &issues_model.IssuesOptions{
   110  		RepoIDs:  []int64{repo.ID},
   111  		IsPull:   optional.Some(false),
   112  		SortType: "oldest",
   113  	})
   114  	assert.NoError(t, err)
   115  	assert.Len(t, issues, 15)
   116  	assert.NoError(t, issues[0].LoadDiscussComments(db.DefaultContext))
   117  	assert.Empty(t, issues[0].Comments)
   118  
   119  	pulls, _, err := issues_model.PullRequests(db.DefaultContext, repo.ID, &issues_model.PullRequestsOptions{
   120  		SortType: "oldest",
   121  	})
   122  	assert.NoError(t, err)
   123  	assert.Len(t, pulls, 30)
   124  	assert.NoError(t, pulls[0].LoadIssue(db.DefaultContext))
   125  	assert.NoError(t, pulls[0].Issue.LoadDiscussComments(db.DefaultContext))
   126  	assert.Len(t, pulls[0].Issue.Comments, 2)
   127  }
   128  
   129  func TestGiteaUploadRemapLocalUser(t *testing.T) {
   130  	unittest.PrepareTestEnv(t)
   131  	doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
   132  	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
   133  
   134  	repoName := "migrated"
   135  	uploader := NewGiteaLocalUploader(context.Background(), doer, doer.Name, repoName)
   136  	// call remapLocalUser
   137  	uploader.sameApp = true
   138  
   139  	externalID := int64(1234567)
   140  	externalName := "username"
   141  	source := base.Release{
   142  		PublisherID:   externalID,
   143  		PublisherName: externalName,
   144  	}
   145  
   146  	//
   147  	// The externalID does not match any existing user, everything
   148  	// belongs to the doer
   149  	//
   150  	target := repo_model.Release{}
   151  	uploader.userMap = make(map[int64]int64)
   152  	err := uploader.remapUser(&source, &target)
   153  	assert.NoError(t, err)
   154  	assert.EqualValues(t, doer.ID, target.GetUserID())
   155  
   156  	//
   157  	// The externalID matches a known user but the name does not match,
   158  	// everything belongs to the doer
   159  	//
   160  	source.PublisherID = user.ID
   161  	target = repo_model.Release{}
   162  	uploader.userMap = make(map[int64]int64)
   163  	err = uploader.remapUser(&source, &target)
   164  	assert.NoError(t, err)
   165  	assert.EqualValues(t, doer.ID, target.GetUserID())
   166  
   167  	//
   168  	// The externalID and externalName match an existing user, everything
   169  	// belongs to the existing user
   170  	//
   171  	source.PublisherName = user.Name
   172  	target = repo_model.Release{}
   173  	uploader.userMap = make(map[int64]int64)
   174  	err = uploader.remapUser(&source, &target)
   175  	assert.NoError(t, err)
   176  	assert.EqualValues(t, user.ID, target.GetUserID())
   177  }
   178  
   179  func TestGiteaUploadRemapExternalUser(t *testing.T) {
   180  	unittest.PrepareTestEnv(t)
   181  	doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
   182  
   183  	repoName := "migrated"
   184  	uploader := NewGiteaLocalUploader(context.Background(), doer, doer.Name, repoName)
   185  	uploader.gitServiceType = structs.GiteaService
   186  	// call remapExternalUser
   187  	uploader.sameApp = false
   188  
   189  	externalID := int64(1234567)
   190  	externalName := "username"
   191  	source := base.Release{
   192  		PublisherID:   externalID,
   193  		PublisherName: externalName,
   194  	}
   195  
   196  	//
   197  	// When there is no user linked to the external ID, the migrated data is authored
   198  	// by the doer
   199  	//
   200  	uploader.userMap = make(map[int64]int64)
   201  	target := repo_model.Release{}
   202  	err := uploader.remapUser(&source, &target)
   203  	assert.NoError(t, err)
   204  	assert.EqualValues(t, doer.ID, target.GetUserID())
   205  
   206  	//
   207  	// Link the external ID to an existing user
   208  	//
   209  	linkedUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
   210  	externalLoginUser := &user_model.ExternalLoginUser{
   211  		ExternalID:    strconv.FormatInt(externalID, 10),
   212  		UserID:        linkedUser.ID,
   213  		LoginSourceID: 0,
   214  		Provider:      structs.GiteaService.Name(),
   215  	}
   216  	err = user_model.LinkExternalToUser(db.DefaultContext, linkedUser, externalLoginUser)
   217  	assert.NoError(t, err)
   218  
   219  	//
   220  	// When a user is linked to the external ID, it becomes the author of
   221  	// the migrated data
   222  	//
   223  	uploader.userMap = make(map[int64]int64)
   224  	target = repo_model.Release{}
   225  	err = uploader.remapUser(&source, &target)
   226  	assert.NoError(t, err)
   227  	assert.EqualValues(t, linkedUser.ID, target.GetUserID())
   228  }
   229  
   230  func TestGiteaUploadUpdateGitForPullRequest(t *testing.T) {
   231  	unittest.PrepareTestEnv(t)
   232  
   233  	//
   234  	// fromRepo master
   235  	//
   236  	fromRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
   237  	baseRef := "master"
   238  	assert.NoError(t, git.InitRepository(git.DefaultContext, fromRepo.RepoPath(), false, fromRepo.ObjectFormatName))
   239  	err := git.NewCommand(git.DefaultContext, "symbolic-ref").AddDynamicArguments("HEAD", git.BranchPrefix+baseRef).Run(&git.RunOpts{Dir: fromRepo.RepoPath()})
   240  	assert.NoError(t, err)
   241  	assert.NoError(t, os.WriteFile(filepath.Join(fromRepo.RepoPath(), "README.md"), []byte(fmt.Sprintf("# Testing Repository\n\nOriginally created in: %s", fromRepo.RepoPath())), 0o644))
   242  	assert.NoError(t, git.AddChanges(fromRepo.RepoPath(), true))
   243  	signature := git.Signature{
   244  		Email: "test@example.com",
   245  		Name:  "test",
   246  		When:  time.Now(),
   247  	}
   248  	assert.NoError(t, git.CommitChanges(fromRepo.RepoPath(), git.CommitChangesOptions{
   249  		Committer: &signature,
   250  		Author:    &signature,
   251  		Message:   "Initial Commit",
   252  	}))
   253  	fromGitRepo, err := gitrepo.OpenRepository(git.DefaultContext, fromRepo)
   254  	assert.NoError(t, err)
   255  	defer fromGitRepo.Close()
   256  	baseSHA, err := fromGitRepo.GetBranchCommitID(baseRef)
   257  	assert.NoError(t, err)
   258  
   259  	//
   260  	// fromRepo branch1
   261  	//
   262  	headRef := "branch1"
   263  	_, _, err = git.NewCommand(git.DefaultContext, "checkout", "-b").AddDynamicArguments(headRef).RunStdString(&git.RunOpts{Dir: fromRepo.RepoPath()})
   264  	assert.NoError(t, err)
   265  	assert.NoError(t, os.WriteFile(filepath.Join(fromRepo.RepoPath(), "README.md"), []byte("SOMETHING"), 0o644))
   266  	assert.NoError(t, git.AddChanges(fromRepo.RepoPath(), true))
   267  	signature.When = time.Now()
   268  	assert.NoError(t, git.CommitChanges(fromRepo.RepoPath(), git.CommitChangesOptions{
   269  		Committer: &signature,
   270  		Author:    &signature,
   271  		Message:   "Pull request",
   272  	}))
   273  	assert.NoError(t, err)
   274  	headSHA, err := fromGitRepo.GetBranchCommitID(headRef)
   275  	assert.NoError(t, err)
   276  
   277  	fromRepoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: fromRepo.OwnerID})
   278  
   279  	//
   280  	// forkRepo branch2
   281  	//
   282  	forkHeadRef := "branch2"
   283  	forkRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 8})
   284  	assert.NoError(t, git.CloneWithArgs(git.DefaultContext, nil, fromRepo.RepoPath(), forkRepo.RepoPath(), git.CloneRepoOptions{
   285  		Branch: headRef,
   286  	}))
   287  	_, _, err = git.NewCommand(git.DefaultContext, "checkout", "-b").AddDynamicArguments(forkHeadRef).RunStdString(&git.RunOpts{Dir: forkRepo.RepoPath()})
   288  	assert.NoError(t, err)
   289  	assert.NoError(t, os.WriteFile(filepath.Join(forkRepo.RepoPath(), "README.md"), []byte(fmt.Sprintf("# branch2 %s", forkRepo.RepoPath())), 0o644))
   290  	assert.NoError(t, git.AddChanges(forkRepo.RepoPath(), true))
   291  	assert.NoError(t, git.CommitChanges(forkRepo.RepoPath(), git.CommitChangesOptions{
   292  		Committer: &signature,
   293  		Author:    &signature,
   294  		Message:   "branch2 commit",
   295  	}))
   296  	forkGitRepo, err := gitrepo.OpenRepository(git.DefaultContext, forkRepo)
   297  	assert.NoError(t, err)
   298  	defer forkGitRepo.Close()
   299  	forkHeadSHA, err := forkGitRepo.GetBranchCommitID(forkHeadRef)
   300  	assert.NoError(t, err)
   301  
   302  	toRepoName := "migrated"
   303  	uploader := NewGiteaLocalUploader(context.Background(), fromRepoOwner, fromRepoOwner.Name, toRepoName)
   304  	uploader.gitServiceType = structs.GiteaService
   305  	assert.NoError(t, uploader.CreateRepo(&base.Repository{
   306  		Description: "description",
   307  		OriginalURL: fromRepo.RepoPath(),
   308  		CloneURL:    fromRepo.RepoPath(),
   309  		IsPrivate:   false,
   310  		IsMirror:    true,
   311  	}, base.MigrateOptions{
   312  		GitServiceType: structs.GiteaService,
   313  		Private:        false,
   314  		Mirror:         true,
   315  	}))
   316  
   317  	for _, testCase := range []struct {
   318  		name        string
   319  		head        string
   320  		logFilter   []string
   321  		logFiltered []bool
   322  		pr          base.PullRequest
   323  	}{
   324  		{
   325  			name: "fork, good Head.SHA",
   326  			head: fmt.Sprintf("%s/%s", forkRepo.OwnerName, forkHeadRef),
   327  			pr: base.PullRequest{
   328  				PatchURL: "",
   329  				Number:   1,
   330  				State:    "open",
   331  				Base: base.PullRequestBranch{
   332  					CloneURL:  fromRepo.RepoPath(),
   333  					Ref:       baseRef,
   334  					SHA:       baseSHA,
   335  					RepoName:  fromRepo.Name,
   336  					OwnerName: fromRepo.OwnerName,
   337  				},
   338  				Head: base.PullRequestBranch{
   339  					CloneURL:  forkRepo.RepoPath(),
   340  					Ref:       forkHeadRef,
   341  					SHA:       forkHeadSHA,
   342  					RepoName:  forkRepo.Name,
   343  					OwnerName: forkRepo.OwnerName,
   344  				},
   345  			},
   346  		},
   347  		{
   348  			name: "fork, invalid Head.Ref",
   349  			head: "unknown repository",
   350  			pr: base.PullRequest{
   351  				PatchURL: "",
   352  				Number:   1,
   353  				State:    "open",
   354  				Base: base.PullRequestBranch{
   355  					CloneURL:  fromRepo.RepoPath(),
   356  					Ref:       baseRef,
   357  					SHA:       baseSHA,
   358  					RepoName:  fromRepo.Name,
   359  					OwnerName: fromRepo.OwnerName,
   360  				},
   361  				Head: base.PullRequestBranch{
   362  					CloneURL:  forkRepo.RepoPath(),
   363  					Ref:       "INVALID",
   364  					SHA:       forkHeadSHA,
   365  					RepoName:  forkRepo.Name,
   366  					OwnerName: forkRepo.OwnerName,
   367  				},
   368  			},
   369  			logFilter:   []string{"Fetch branch from"},
   370  			logFiltered: []bool{true},
   371  		},
   372  		{
   373  			name: "invalid fork CloneURL",
   374  			head: "unknown repository",
   375  			pr: base.PullRequest{
   376  				PatchURL: "",
   377  				Number:   1,
   378  				State:    "open",
   379  				Base: base.PullRequestBranch{
   380  					CloneURL:  fromRepo.RepoPath(),
   381  					Ref:       baseRef,
   382  					SHA:       baseSHA,
   383  					RepoName:  fromRepo.Name,
   384  					OwnerName: fromRepo.OwnerName,
   385  				},
   386  				Head: base.PullRequestBranch{
   387  					CloneURL:  "UNLIKELY",
   388  					Ref:       forkHeadRef,
   389  					SHA:       forkHeadSHA,
   390  					RepoName:  forkRepo.Name,
   391  					OwnerName: "WRONG",
   392  				},
   393  			},
   394  			logFilter:   []string{"AddRemote"},
   395  			logFiltered: []bool{true},
   396  		},
   397  		{
   398  			name: "no fork, good Head.SHA",
   399  			head: headRef,
   400  			pr: base.PullRequest{
   401  				PatchURL: "",
   402  				Number:   1,
   403  				State:    "open",
   404  				Base: base.PullRequestBranch{
   405  					CloneURL:  fromRepo.RepoPath(),
   406  					Ref:       baseRef,
   407  					SHA:       baseSHA,
   408  					RepoName:  fromRepo.Name,
   409  					OwnerName: fromRepo.OwnerName,
   410  				},
   411  				Head: base.PullRequestBranch{
   412  					CloneURL:  fromRepo.RepoPath(),
   413  					Ref:       headRef,
   414  					SHA:       headSHA,
   415  					RepoName:  fromRepo.Name,
   416  					OwnerName: fromRepo.OwnerName,
   417  				},
   418  			},
   419  		},
   420  		{
   421  			name: "no fork, empty Head.SHA",
   422  			head: headRef,
   423  			pr: base.PullRequest{
   424  				PatchURL: "",
   425  				Number:   1,
   426  				State:    "open",
   427  				Base: base.PullRequestBranch{
   428  					CloneURL:  fromRepo.RepoPath(),
   429  					Ref:       baseRef,
   430  					SHA:       baseSHA,
   431  					RepoName:  fromRepo.Name,
   432  					OwnerName: fromRepo.OwnerName,
   433  				},
   434  				Head: base.PullRequestBranch{
   435  					CloneURL:  fromRepo.RepoPath(),
   436  					Ref:       headRef,
   437  					SHA:       "",
   438  					RepoName:  fromRepo.Name,
   439  					OwnerName: fromRepo.OwnerName,
   440  				},
   441  			},
   442  			logFilter:   []string{"Empty reference", "Cannot remove local head"},
   443  			logFiltered: []bool{true, false},
   444  		},
   445  		{
   446  			name: "no fork, invalid Head.SHA",
   447  			head: headRef,
   448  			pr: base.PullRequest{
   449  				PatchURL: "",
   450  				Number:   1,
   451  				State:    "open",
   452  				Base: base.PullRequestBranch{
   453  					CloneURL:  fromRepo.RepoPath(),
   454  					Ref:       baseRef,
   455  					SHA:       baseSHA,
   456  					RepoName:  fromRepo.Name,
   457  					OwnerName: fromRepo.OwnerName,
   458  				},
   459  				Head: base.PullRequestBranch{
   460  					CloneURL:  fromRepo.RepoPath(),
   461  					Ref:       headRef,
   462  					SHA:       "brokenSHA",
   463  					RepoName:  fromRepo.Name,
   464  					OwnerName: fromRepo.OwnerName,
   465  				},
   466  			},
   467  			logFilter:   []string{"Deprecated local head"},
   468  			logFiltered: []bool{true},
   469  		},
   470  		{
   471  			name: "no fork, not found Head.SHA",
   472  			head: headRef,
   473  			pr: base.PullRequest{
   474  				PatchURL: "",
   475  				Number:   1,
   476  				State:    "open",
   477  				Base: base.PullRequestBranch{
   478  					CloneURL:  fromRepo.RepoPath(),
   479  					Ref:       baseRef,
   480  					SHA:       baseSHA,
   481  					RepoName:  fromRepo.Name,
   482  					OwnerName: fromRepo.OwnerName,
   483  				},
   484  				Head: base.PullRequestBranch{
   485  					CloneURL:  fromRepo.RepoPath(),
   486  					Ref:       headRef,
   487  					SHA:       "2697b352310fcd01cbd1f3dbd43b894080027f68",
   488  					RepoName:  fromRepo.Name,
   489  					OwnerName: fromRepo.OwnerName,
   490  				},
   491  			},
   492  			logFilter:   []string{"Deprecated local head", "Cannot remove local head"},
   493  			logFiltered: []bool{true, false},
   494  		},
   495  	} {
   496  		t.Run(testCase.name, func(t *testing.T) {
   497  			stopMark := fmt.Sprintf(">>>>>>>>>>>>>STOP: %s<<<<<<<<<<<<<<<", testCase.name)
   498  
   499  			logChecker, cleanup := test.NewLogChecker(log.DEFAULT)
   500  			logChecker.Filter(testCase.logFilter...).StopMark(stopMark)
   501  			defer cleanup()
   502  
   503  			testCase.pr.EnsuredSafe = true
   504  
   505  			head, err := uploader.updateGitForPullRequest(&testCase.pr)
   506  			assert.NoError(t, err)
   507  			assert.EqualValues(t, testCase.head, head)
   508  
   509  			log.Info(stopMark)
   510  
   511  			logFiltered, logStopped := logChecker.Check(5 * time.Second)
   512  			assert.True(t, logStopped)
   513  			if len(testCase.logFilter) > 0 {
   514  				assert.EqualValues(t, testCase.logFiltered, logFiltered, "for log message filters: %v", testCase.logFilter)
   515  			}
   516  		})
   517  	}
   518  }