Платформа ЦРНП "Мирокод" для разработки проектов
https://git.mirocod.ru
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.
328 lines
9.7 KiB
328 lines
9.7 KiB
// Copyright 2022 The Gitea Authors. All rights reserved. |
|
// Use of this source code is governed by a MIT-style |
|
// license that can be found in the LICENSE file. |
|
|
|
package integrations |
|
|
|
import ( |
|
"context" |
|
"errors" |
|
"fmt" |
|
"net/url" |
|
"os" |
|
"path/filepath" |
|
"reflect" |
|
"strings" |
|
"testing" |
|
|
|
repo_model "code.gitea.io/gitea/models/repo" |
|
"code.gitea.io/gitea/models/unittest" |
|
user_model "code.gitea.io/gitea/models/user" |
|
base "code.gitea.io/gitea/modules/migration" |
|
"code.gitea.io/gitea/modules/setting" |
|
"code.gitea.io/gitea/modules/structs" |
|
"code.gitea.io/gitea/modules/util" |
|
"code.gitea.io/gitea/services/migrations" |
|
|
|
"github.com/stretchr/testify/assert" |
|
"gopkg.in/yaml.v2" |
|
) |
|
|
|
func TestDumpRestore(t *testing.T) { |
|
onGiteaRun(t, func(t *testing.T, u *url.URL) { |
|
AllowLocalNetworks := setting.Migrations.AllowLocalNetworks |
|
setting.Migrations.AllowLocalNetworks = true |
|
AppVer := setting.AppVer |
|
// Gitea SDK (go-sdk) need to parse the AppVer from server response, so we must set it to a valid version string. |
|
setting.AppVer = "1.16.0" |
|
defer func() { |
|
setting.Migrations.AllowLocalNetworks = AllowLocalNetworks |
|
setting.AppVer = AppVer |
|
}() |
|
|
|
assert.NoError(t, migrations.Init()) |
|
|
|
reponame := "repo1" |
|
|
|
basePath, err := os.MkdirTemp("", reponame) |
|
assert.NoError(t, err) |
|
defer util.RemoveAll(basePath) |
|
|
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: reponame}).(*repo_model.Repository) |
|
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}).(*user_model.User) |
|
session := loginUser(t, repoOwner.Name) |
|
token := getTokenForLoggedInUser(t, session) |
|
|
|
// |
|
// Phase 1: dump repo1 from the Gitea instance to the filesystem |
|
// |
|
|
|
ctx := context.Background() |
|
opts := migrations.MigrateOptions{ |
|
GitServiceType: structs.GiteaService, |
|
Issues: true, |
|
PullRequests: true, |
|
Labels: true, |
|
Milestones: true, |
|
Comments: true, |
|
AuthToken: token, |
|
CloneAddr: repo.CloneLink().HTTPS, |
|
RepoName: reponame, |
|
} |
|
err = migrations.DumpRepository(ctx, basePath, repoOwner.Name, opts) |
|
assert.NoError(t, err) |
|
|
|
// |
|
// Verify desired side effects of the dump |
|
// |
|
d := filepath.Join(basePath, repo.OwnerName, repo.Name) |
|
for _, f := range []string{"repo.yml", "topic.yml", "label.yml", "milestone.yml", "issue.yml"} { |
|
assert.FileExists(t, filepath.Join(d, f)) |
|
} |
|
|
|
// |
|
// Phase 2: restore from the filesystem to the Gitea instance in restoredrepo |
|
// |
|
|
|
newreponame := "restored" |
|
err = migrations.RestoreRepository(ctx, d, repo.OwnerName, newreponame, []string{ |
|
"labels", "issues", "comments", "milestones", "pull_requests", |
|
}, false) |
|
assert.NoError(t, err) |
|
|
|
newrepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: newreponame}).(*repo_model.Repository) |
|
|
|
// |
|
// Phase 3: dump restored from the Gitea instance to the filesystem |
|
// |
|
opts.RepoName = newreponame |
|
opts.CloneAddr = newrepo.CloneLink().HTTPS |
|
err = migrations.DumpRepository(ctx, basePath, repoOwner.Name, opts) |
|
assert.NoError(t, err) |
|
|
|
// |
|
// Verify the dump of restored is the same as the dump of repo1 |
|
// |
|
comparator := &compareDump{ |
|
t: t, |
|
basePath: basePath, |
|
} |
|
comparator.assertEquals(repo, newrepo) |
|
}) |
|
} |
|
|
|
type compareDump struct { |
|
t *testing.T |
|
basePath string |
|
repoBefore *repo_model.Repository |
|
dirBefore string |
|
repoAfter *repo_model.Repository |
|
dirAfter string |
|
} |
|
|
|
type compareField struct { |
|
before interface{} |
|
after interface{} |
|
ignore bool |
|
transform func(string) string |
|
nested *compareFields |
|
} |
|
|
|
type compareFields map[string]compareField |
|
|
|
func (c *compareDump) replaceRepoName(original string) string { |
|
return strings.ReplaceAll(original, c.repoBefore.Name, c.repoAfter.Name) |
|
} |
|
|
|
func (c *compareDump) assertEquals(repoBefore, repoAfter *repo_model.Repository) { |
|
c.repoBefore = repoBefore |
|
c.dirBefore = filepath.Join(c.basePath, repoBefore.OwnerName, repoBefore.Name) |
|
c.repoAfter = repoAfter |
|
c.dirAfter = filepath.Join(c.basePath, repoAfter.OwnerName, repoAfter.Name) |
|
|
|
// |
|
// base.Repository |
|
// |
|
_ = c.assertEqual("repo.yml", base.Repository{}, compareFields{ |
|
"Name": { |
|
before: c.repoBefore.Name, |
|
after: c.repoAfter.Name, |
|
}, |
|
"CloneURL": {transform: c.replaceRepoName}, |
|
"OriginalURL": {transform: c.replaceRepoName}, |
|
}) |
|
|
|
// |
|
// base.Label |
|
// |
|
labels, ok := c.assertEqual("label.yml", []base.Label{}, compareFields{}).([]*base.Label) |
|
assert.True(c.t, ok) |
|
assert.GreaterOrEqual(c.t, len(labels), 1) |
|
|
|
// |
|
// base.Milestone |
|
// |
|
milestones, ok := c.assertEqual("milestone.yml", []base.Milestone{}, compareFields{ |
|
"Updated": {ignore: true}, // the database updates that field independently |
|
}).([]*base.Milestone) |
|
assert.True(c.t, ok) |
|
assert.GreaterOrEqual(c.t, len(milestones), 1) |
|
|
|
// |
|
// base.Issue and the associated comments |
|
// |
|
issues, ok := c.assertEqual("issue.yml", []base.Issue{}, compareFields{ |
|
"Assignees": {ignore: true}, // not implemented yet |
|
}).([]*base.Issue) |
|
assert.True(c.t, ok) |
|
assert.GreaterOrEqual(c.t, len(issues), 1) |
|
for _, issue := range issues { |
|
filename := filepath.Join("comments", fmt.Sprintf("%d.yml", issue.Number)) |
|
comments, ok := c.assertEqual(filename, []base.Comment{}, compareFields{ |
|
"Index": {ignore: true}, |
|
}).([]*base.Comment) |
|
assert.True(c.t, ok) |
|
for _, comment := range comments { |
|
assert.EqualValues(c.t, issue.Number, comment.IssueIndex) |
|
} |
|
} |
|
|
|
// |
|
// base.PullRequest and the associated comments |
|
// |
|
comparePullRequestBranch := &compareFields{ |
|
"RepoName": { |
|
before: c.repoBefore.Name, |
|
after: c.repoAfter.Name, |
|
}, |
|
"CloneURL": {transform: c.replaceRepoName}, |
|
} |
|
prs, ok := c.assertEqual("pull_request.yml", []base.PullRequest{}, compareFields{ |
|
"Assignees": {ignore: true}, // not implemented yet |
|
"Head": {nested: comparePullRequestBranch}, |
|
"Base": {nested: comparePullRequestBranch}, |
|
"Labels": {ignore: true}, // because org labels are not handled propery |
|
}).([]*base.PullRequest) |
|
assert.True(c.t, ok) |
|
assert.GreaterOrEqual(c.t, len(prs), 1) |
|
for _, pr := range prs { |
|
filename := filepath.Join("comments", fmt.Sprintf("%d.yml", pr.Number)) |
|
comments, ok := c.assertEqual(filename, []base.Comment{}, compareFields{}).([]*base.Comment) |
|
assert.True(c.t, ok) |
|
for _, comment := range comments { |
|
assert.EqualValues(c.t, pr.Number, comment.IssueIndex) |
|
} |
|
} |
|
} |
|
|
|
func (c *compareDump) assertLoadYAMLFiles(beforeFilename, afterFilename string, before, after interface{}) { |
|
_, beforeErr := os.Stat(beforeFilename) |
|
_, afterErr := os.Stat(afterFilename) |
|
assert.EqualValues(c.t, errors.Is(beforeErr, os.ErrNotExist), errors.Is(afterErr, os.ErrNotExist)) |
|
if errors.Is(beforeErr, os.ErrNotExist) { |
|
return |
|
} |
|
|
|
beforeBytes, err := os.ReadFile(beforeFilename) |
|
assert.NoError(c.t, err) |
|
assert.NoError(c.t, yaml.Unmarshal(beforeBytes, before)) |
|
afterBytes, err := os.ReadFile(afterFilename) |
|
assert.NoError(c.t, err) |
|
assert.NoError(c.t, yaml.Unmarshal(afterBytes, after)) |
|
} |
|
|
|
func (c *compareDump) assertLoadFiles(beforeFilename, afterFilename string, t reflect.Type) (before, after reflect.Value) { |
|
var beforePtr, afterPtr reflect.Value |
|
if t.Kind() == reflect.Slice { |
|
// |
|
// Given []Something{} create afterPtr, beforePtr []*Something{} |
|
// |
|
sliceType := reflect.SliceOf(reflect.PtrTo(t.Elem())) |
|
beforeSlice := reflect.MakeSlice(sliceType, 0, 10) |
|
beforePtr = reflect.New(beforeSlice.Type()) |
|
beforePtr.Elem().Set(beforeSlice) |
|
afterSlice := reflect.MakeSlice(sliceType, 0, 10) |
|
afterPtr = reflect.New(afterSlice.Type()) |
|
afterPtr.Elem().Set(afterSlice) |
|
} else { |
|
// |
|
// Given Something{} create afterPtr, beforePtr *Something{} |
|
// |
|
beforePtr = reflect.New(t) |
|
afterPtr = reflect.New(t) |
|
} |
|
c.assertLoadYAMLFiles(beforeFilename, afterFilename, beforePtr.Interface(), afterPtr.Interface()) |
|
return beforePtr.Elem(), afterPtr.Elem() |
|
} |
|
|
|
func (c *compareDump) assertEqual(filename string, kind interface{}, fields compareFields) (i interface{}) { |
|
beforeFilename := filepath.Join(c.dirBefore, filename) |
|
afterFilename := filepath.Join(c.dirAfter, filename) |
|
|
|
typeOf := reflect.TypeOf(kind) |
|
before, after := c.assertLoadFiles(beforeFilename, afterFilename, typeOf) |
|
if typeOf.Kind() == reflect.Slice { |
|
i = c.assertEqualSlices(before, after, fields) |
|
} else { |
|
i = c.assertEqualValues(before, after, fields) |
|
} |
|
return i |
|
} |
|
|
|
func (c *compareDump) assertEqualSlices(before, after reflect.Value, fields compareFields) interface{} { |
|
assert.EqualValues(c.t, before.Len(), after.Len()) |
|
if before.Len() == after.Len() { |
|
for i := 0; i < before.Len(); i++ { |
|
_ = c.assertEqualValues( |
|
reflect.Indirect(before.Index(i).Elem()), |
|
reflect.Indirect(after.Index(i).Elem()), |
|
fields) |
|
} |
|
} |
|
return after.Interface() |
|
} |
|
|
|
func (c *compareDump) assertEqualValues(before, after reflect.Value, fields compareFields) interface{} { |
|
for _, field := range reflect.VisibleFields(before.Type()) { |
|
bf := before.FieldByName(field.Name) |
|
bi := bf.Interface() |
|
af := after.FieldByName(field.Name) |
|
ai := af.Interface() |
|
if compare, ok := fields[field.Name]; ok { |
|
if compare.ignore == true { |
|
// |
|
// Ignore |
|
// |
|
continue |
|
} |
|
if compare.transform != nil { |
|
// |
|
// Transform these strings before comparing them |
|
// |
|
bs, ok := bi.(string) |
|
assert.True(c.t, ok) |
|
as, ok := ai.(string) |
|
assert.True(c.t, ok) |
|
assert.EqualValues(c.t, compare.transform(bs), compare.transform(as)) |
|
continue |
|
} |
|
if compare.before != nil && compare.after != nil { |
|
// |
|
// The fields are expected to have different values |
|
// |
|
assert.EqualValues(c.t, compare.before, bi) |
|
assert.EqualValues(c.t, compare.after, ai) |
|
continue |
|
} |
|
if compare.nested != nil { |
|
// |
|
// The fields are a struct, recurse |
|
// |
|
c.assertEqualValues(bf, af, *compare.nested) |
|
continue |
|
} |
|
} |
|
assert.EqualValues(c.t, bi, ai) |
|
} |
|
return after.Interface() |
|
}
|
|
|