code.gitea.io/gitea@v1.22.3/tests/integration/dump_restore_test.go (about)

     1  // Copyright 2022 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package integration
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"fmt"
    10  	"net/url"
    11  	"os"
    12  	"path/filepath"
    13  	"reflect"
    14  	"strings"
    15  	"testing"
    16  
    17  	auth_model "code.gitea.io/gitea/models/auth"
    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  	base "code.gitea.io/gitea/modules/migration"
    22  	"code.gitea.io/gitea/modules/setting"
    23  	"code.gitea.io/gitea/modules/structs"
    24  	"code.gitea.io/gitea/modules/util"
    25  	"code.gitea.io/gitea/services/migrations"
    26  
    27  	"github.com/stretchr/testify/assert"
    28  	"gopkg.in/yaml.v3"
    29  )
    30  
    31  func TestDumpRestore(t *testing.T) {
    32  	onGiteaRun(t, func(t *testing.T, u *url.URL) {
    33  		AllowLocalNetworks := setting.Migrations.AllowLocalNetworks
    34  		setting.Migrations.AllowLocalNetworks = true
    35  		AppVer := setting.AppVer
    36  		// Gitea SDK (go-sdk) need to parse the AppVer from server response, so we must set it to a valid version string.
    37  		setting.AppVer = "1.16.0"
    38  		defer func() {
    39  			setting.Migrations.AllowLocalNetworks = AllowLocalNetworks
    40  			setting.AppVer = AppVer
    41  		}()
    42  
    43  		assert.NoError(t, migrations.Init())
    44  
    45  		reponame := "repo1"
    46  
    47  		basePath, err := os.MkdirTemp("", reponame)
    48  		assert.NoError(t, err)
    49  		defer util.RemoveAll(basePath)
    50  
    51  		repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: reponame})
    52  		repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
    53  		session := loginUser(t, repoOwner.Name)
    54  		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeReadMisc)
    55  
    56  		//
    57  		// Phase 1: dump repo1 from the Gitea instance to the filesystem
    58  		//
    59  
    60  		ctx := context.Background()
    61  		opts := migrations.MigrateOptions{
    62  			GitServiceType: structs.GiteaService,
    63  			Issues:         true,
    64  			PullRequests:   true,
    65  			Labels:         true,
    66  			Milestones:     true,
    67  			Comments:       true,
    68  			AuthToken:      token,
    69  			CloneAddr:      repo.CloneLink().HTTPS,
    70  			RepoName:       reponame,
    71  		}
    72  		err = migrations.DumpRepository(ctx, basePath, repoOwner.Name, opts)
    73  		assert.NoError(t, err)
    74  
    75  		//
    76  		// Verify desired side effects of the dump
    77  		//
    78  		d := filepath.Join(basePath, repo.OwnerName, repo.Name)
    79  		for _, f := range []string{"repo.yml", "topic.yml", "label.yml", "milestone.yml", "issue.yml"} {
    80  			assert.FileExists(t, filepath.Join(d, f))
    81  		}
    82  
    83  		//
    84  		// Phase 2: restore from the filesystem to the Gitea instance in restoredrepo
    85  		//
    86  
    87  		newreponame := "restored"
    88  		err = migrations.RestoreRepository(ctx, d, repo.OwnerName, newreponame, []string{
    89  			"labels", "issues", "comments", "milestones", "pull_requests",
    90  		}, false)
    91  		assert.NoError(t, err)
    92  
    93  		newrepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: newreponame})
    94  
    95  		//
    96  		// Phase 3: dump restored from the Gitea instance to the filesystem
    97  		//
    98  		opts.RepoName = newreponame
    99  		opts.CloneAddr = newrepo.CloneLink().HTTPS
   100  		err = migrations.DumpRepository(ctx, basePath, repoOwner.Name, opts)
   101  		assert.NoError(t, err)
   102  
   103  		//
   104  		// Verify the dump of restored is the same as the dump of repo1
   105  		//
   106  		comparator := &compareDump{
   107  			t:        t,
   108  			basePath: basePath,
   109  		}
   110  		comparator.assertEquals(repo, newrepo)
   111  	})
   112  }
   113  
   114  type compareDump struct {
   115  	t          *testing.T
   116  	basePath   string
   117  	repoBefore *repo_model.Repository
   118  	dirBefore  string
   119  	repoAfter  *repo_model.Repository
   120  	dirAfter   string
   121  }
   122  
   123  type compareField struct {
   124  	before    any
   125  	after     any
   126  	ignore    bool
   127  	transform func(string) string
   128  	nested    *compareFields
   129  }
   130  
   131  type compareFields map[string]compareField
   132  
   133  func (c *compareDump) replaceRepoName(original string) string {
   134  	return strings.ReplaceAll(original, c.repoBefore.Name, c.repoAfter.Name)
   135  }
   136  
   137  func (c *compareDump) assertEquals(repoBefore, repoAfter *repo_model.Repository) {
   138  	c.repoBefore = repoBefore
   139  	c.dirBefore = filepath.Join(c.basePath, repoBefore.OwnerName, repoBefore.Name)
   140  	c.repoAfter = repoAfter
   141  	c.dirAfter = filepath.Join(c.basePath, repoAfter.OwnerName, repoAfter.Name)
   142  
   143  	//
   144  	// base.Repository
   145  	//
   146  	_ = c.assertEqual("repo.yml", base.Repository{}, compareFields{
   147  		"Name": {
   148  			before: c.repoBefore.Name,
   149  			after:  c.repoAfter.Name,
   150  		},
   151  		"CloneURL":    {transform: c.replaceRepoName},
   152  		"OriginalURL": {transform: c.replaceRepoName},
   153  	})
   154  
   155  	//
   156  	// base.Label
   157  	//
   158  	labels, ok := c.assertEqual("label.yml", []base.Label{}, compareFields{}).([]*base.Label)
   159  	assert.True(c.t, ok)
   160  	assert.GreaterOrEqual(c.t, len(labels), 1)
   161  
   162  	//
   163  	// base.Milestone
   164  	//
   165  	milestones, ok := c.assertEqual("milestone.yml", []base.Milestone{}, compareFields{
   166  		"Updated": {ignore: true}, // the database updates that field independently
   167  	}).([]*base.Milestone)
   168  	assert.True(c.t, ok)
   169  	assert.GreaterOrEqual(c.t, len(milestones), 1)
   170  
   171  	//
   172  	// base.Issue and the associated comments
   173  	//
   174  	issues, ok := c.assertEqual("issue.yml", []base.Issue{}, compareFields{
   175  		"Assignees": {ignore: true}, // not implemented yet
   176  	}).([]*base.Issue)
   177  	assert.True(c.t, ok)
   178  	assert.GreaterOrEqual(c.t, len(issues), 1)
   179  	for _, issue := range issues {
   180  		filename := filepath.Join("comments", fmt.Sprintf("%d.yml", issue.Number))
   181  		comments, ok := c.assertEqual(filename, []base.Comment{}, compareFields{
   182  			"Index": {ignore: true},
   183  		}).([]*base.Comment)
   184  		assert.True(c.t, ok)
   185  		for _, comment := range comments {
   186  			assert.EqualValues(c.t, issue.Number, comment.IssueIndex)
   187  		}
   188  	}
   189  
   190  	//
   191  	// base.PullRequest and the associated comments
   192  	//
   193  	comparePullRequestBranch := &compareFields{
   194  		"RepoName": {
   195  			before: c.repoBefore.Name,
   196  			after:  c.repoAfter.Name,
   197  		},
   198  		"CloneURL": {transform: c.replaceRepoName},
   199  	}
   200  	prs, ok := c.assertEqual("pull_request.yml", []base.PullRequest{}, compareFields{
   201  		"Assignees": {ignore: true}, // not implemented yet
   202  		"Head":      {nested: comparePullRequestBranch},
   203  		"Base":      {nested: comparePullRequestBranch},
   204  		"Labels":    {ignore: true}, // because org labels are not handled properly
   205  	}).([]*base.PullRequest)
   206  	assert.True(c.t, ok)
   207  	assert.GreaterOrEqual(c.t, len(prs), 1)
   208  	for _, pr := range prs {
   209  		filename := filepath.Join("comments", fmt.Sprintf("%d.yml", pr.Number))
   210  		comments, ok := c.assertEqual(filename, []base.Comment{}, compareFields{}).([]*base.Comment)
   211  		assert.True(c.t, ok)
   212  		for _, comment := range comments {
   213  			assert.EqualValues(c.t, pr.Number, comment.IssueIndex)
   214  		}
   215  	}
   216  }
   217  
   218  func (c *compareDump) assertLoadYAMLFiles(beforeFilename, afterFilename string, before, after any) {
   219  	_, beforeErr := os.Stat(beforeFilename)
   220  	_, afterErr := os.Stat(afterFilename)
   221  	assert.EqualValues(c.t, errors.Is(beforeErr, os.ErrNotExist), errors.Is(afterErr, os.ErrNotExist))
   222  	if errors.Is(beforeErr, os.ErrNotExist) {
   223  		return
   224  	}
   225  
   226  	beforeBytes, err := os.ReadFile(beforeFilename)
   227  	assert.NoError(c.t, err)
   228  	assert.NoError(c.t, yaml.Unmarshal(beforeBytes, before))
   229  	afterBytes, err := os.ReadFile(afterFilename)
   230  	assert.NoError(c.t, err)
   231  	assert.NoError(c.t, yaml.Unmarshal(afterBytes, after))
   232  }
   233  
   234  func (c *compareDump) assertLoadFiles(beforeFilename, afterFilename string, t reflect.Type) (before, after reflect.Value) {
   235  	var beforePtr, afterPtr reflect.Value
   236  	if t.Kind() == reflect.Slice {
   237  		//
   238  		// Given []Something{} create afterPtr, beforePtr []*Something{}
   239  		//
   240  		sliceType := reflect.SliceOf(reflect.PtrTo(t.Elem()))
   241  		beforeSlice := reflect.MakeSlice(sliceType, 0, 10)
   242  		beforePtr = reflect.New(beforeSlice.Type())
   243  		beforePtr.Elem().Set(beforeSlice)
   244  		afterSlice := reflect.MakeSlice(sliceType, 0, 10)
   245  		afterPtr = reflect.New(afterSlice.Type())
   246  		afterPtr.Elem().Set(afterSlice)
   247  	} else {
   248  		//
   249  		// Given Something{} create afterPtr, beforePtr *Something{}
   250  		//
   251  		beforePtr = reflect.New(t)
   252  		afterPtr = reflect.New(t)
   253  	}
   254  	c.assertLoadYAMLFiles(beforeFilename, afterFilename, beforePtr.Interface(), afterPtr.Interface())
   255  	return beforePtr.Elem(), afterPtr.Elem()
   256  }
   257  
   258  func (c *compareDump) assertEqual(filename string, kind any, fields compareFields) (i any) {
   259  	beforeFilename := filepath.Join(c.dirBefore, filename)
   260  	afterFilename := filepath.Join(c.dirAfter, filename)
   261  
   262  	typeOf := reflect.TypeOf(kind)
   263  	before, after := c.assertLoadFiles(beforeFilename, afterFilename, typeOf)
   264  	if typeOf.Kind() == reflect.Slice {
   265  		i = c.assertEqualSlices(before, after, fields)
   266  	} else {
   267  		i = c.assertEqualValues(before, after, fields)
   268  	}
   269  	return i
   270  }
   271  
   272  func (c *compareDump) assertEqualSlices(before, after reflect.Value, fields compareFields) any {
   273  	assert.EqualValues(c.t, before.Len(), after.Len())
   274  	if before.Len() == after.Len() {
   275  		for i := 0; i < before.Len(); i++ {
   276  			_ = c.assertEqualValues(
   277  				reflect.Indirect(before.Index(i).Elem()),
   278  				reflect.Indirect(after.Index(i).Elem()),
   279  				fields)
   280  		}
   281  	}
   282  	return after.Interface()
   283  }
   284  
   285  func (c *compareDump) assertEqualValues(before, after reflect.Value, fields compareFields) any {
   286  	for _, field := range reflect.VisibleFields(before.Type()) {
   287  		bf := before.FieldByName(field.Name)
   288  		bi := bf.Interface()
   289  		af := after.FieldByName(field.Name)
   290  		ai := af.Interface()
   291  		if compare, ok := fields[field.Name]; ok {
   292  			if compare.ignore == true {
   293  				//
   294  				// Ignore
   295  				//
   296  				continue
   297  			}
   298  			if compare.transform != nil {
   299  				//
   300  				// Transform these strings before comparing them
   301  				//
   302  				bs, ok := bi.(string)
   303  				assert.True(c.t, ok)
   304  				as, ok := ai.(string)
   305  				assert.True(c.t, ok)
   306  				assert.EqualValues(c.t, compare.transform(bs), compare.transform(as))
   307  				continue
   308  			}
   309  			if compare.before != nil && compare.after != nil {
   310  				//
   311  				// The fields are expected to have different values
   312  				//
   313  				assert.EqualValues(c.t, compare.before, bi)
   314  				assert.EqualValues(c.t, compare.after, ai)
   315  				continue
   316  			}
   317  			if compare.nested != nil {
   318  				//
   319  				// The fields are a struct, recurse
   320  				//
   321  				c.assertEqualValues(bf, af, *compare.nested)
   322  				continue
   323  			}
   324  		}
   325  		assert.EqualValues(c.t, bi, ai)
   326  	}
   327  	return after.Interface()
   328  }