code.gitea.io/gitea@v1.21.7/tests/integration/migration-test/migration_test.go (about)

     1  // Copyright 2019 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package migrations
     5  
     6  import (
     7  	"compress/gzip"
     8  	"context"
     9  	"database/sql"
    10  	"fmt"
    11  	"io"
    12  	"os"
    13  	"path"
    14  	"path/filepath"
    15  	"regexp"
    16  	"sort"
    17  	"strings"
    18  	"testing"
    19  
    20  	"code.gitea.io/gitea/models/db"
    21  	"code.gitea.io/gitea/models/migrations"
    22  	migrate_base "code.gitea.io/gitea/models/migrations/base"
    23  	"code.gitea.io/gitea/models/unittest"
    24  	"code.gitea.io/gitea/modules/base"
    25  	"code.gitea.io/gitea/modules/charset"
    26  	"code.gitea.io/gitea/modules/git"
    27  	"code.gitea.io/gitea/modules/log"
    28  	"code.gitea.io/gitea/modules/setting"
    29  	"code.gitea.io/gitea/modules/testlogger"
    30  	"code.gitea.io/gitea/modules/util"
    31  	"code.gitea.io/gitea/tests"
    32  
    33  	"github.com/stretchr/testify/assert"
    34  	"xorm.io/xorm"
    35  )
    36  
    37  var currentEngine *xorm.Engine
    38  
    39  func initMigrationTest(t *testing.T) func() {
    40  	log.RegisterEventWriter("test", testlogger.NewTestLoggerWriter)
    41  
    42  	deferFn := tests.PrintCurrentTest(t, 2)
    43  	giteaRoot := base.SetupGiteaRoot()
    44  	if giteaRoot == "" {
    45  		tests.Printf("Environment variable $GITEA_ROOT not set\n")
    46  		os.Exit(1)
    47  	}
    48  	setting.AppPath = path.Join(giteaRoot, "gitea")
    49  	if _, err := os.Stat(setting.AppPath); err != nil {
    50  		tests.Printf("Could not find gitea binary at %s\n", setting.AppPath)
    51  		os.Exit(1)
    52  	}
    53  
    54  	giteaConf := os.Getenv("GITEA_CONF")
    55  	if giteaConf == "" {
    56  		tests.Printf("Environment variable $GITEA_CONF not set\n")
    57  		os.Exit(1)
    58  	} else if !path.IsAbs(giteaConf) {
    59  		setting.CustomConf = path.Join(giteaRoot, giteaConf)
    60  	} else {
    61  		setting.CustomConf = giteaConf
    62  	}
    63  
    64  	unittest.InitSettings()
    65  
    66  	assert.True(t, len(setting.RepoRootPath) != 0)
    67  	assert.NoError(t, util.RemoveAll(setting.RepoRootPath))
    68  	assert.NoError(t, unittest.CopyDir(path.Join(filepath.Dir(setting.AppPath), "tests/gitea-repositories-meta"), setting.RepoRootPath))
    69  	ownerDirs, err := os.ReadDir(setting.RepoRootPath)
    70  	if err != nil {
    71  		assert.NoError(t, err, "unable to read the new repo root: %v\n", err)
    72  	}
    73  	for _, ownerDir := range ownerDirs {
    74  		if !ownerDir.Type().IsDir() {
    75  			continue
    76  		}
    77  		repoDirs, err := os.ReadDir(filepath.Join(setting.RepoRootPath, ownerDir.Name()))
    78  		if err != nil {
    79  			assert.NoError(t, err, "unable to read the new repo root: %v\n", err)
    80  		}
    81  		for _, repoDir := range repoDirs {
    82  			_ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "objects", "pack"), 0o755)
    83  			_ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "objects", "info"), 0o755)
    84  			_ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "refs", "heads"), 0o755)
    85  			_ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "refs", "tag"), 0o755)
    86  		}
    87  	}
    88  
    89  	assert.NoError(t, git.InitFull(context.Background()))
    90  	setting.LoadDBSetting()
    91  	setting.InitLoggersForTest()
    92  	return deferFn
    93  }
    94  
    95  func availableVersions() ([]string, error) {
    96  	migrationsDir, err := os.Open("tests/integration/migration-test")
    97  	if err != nil {
    98  		return nil, err
    99  	}
   100  	defer migrationsDir.Close()
   101  	versionRE, err := regexp.Compile("gitea-v(?P<version>.+)\\." + regexp.QuoteMeta(setting.Database.Type.String()) + "\\.sql.gz")
   102  	if err != nil {
   103  		return nil, err
   104  	}
   105  
   106  	filenames, err := migrationsDir.Readdirnames(-1)
   107  	if err != nil {
   108  		return nil, err
   109  	}
   110  	versions := []string{}
   111  	for _, filename := range filenames {
   112  		if versionRE.MatchString(filename) {
   113  			substrings := versionRE.FindStringSubmatch(filename)
   114  			versions = append(versions, substrings[1])
   115  		}
   116  	}
   117  	sort.Strings(versions)
   118  	return versions, nil
   119  }
   120  
   121  func readSQLFromFile(version string) (string, error) {
   122  	filename := fmt.Sprintf("tests/integration/migration-test/gitea-v%s.%s.sql.gz", version, setting.Database.Type)
   123  
   124  	if _, err := os.Stat(filename); os.IsNotExist(err) {
   125  		return "", nil
   126  	}
   127  
   128  	file, err := os.Open(filename)
   129  	if err != nil {
   130  		return "", err
   131  	}
   132  	defer file.Close()
   133  
   134  	gr, err := gzip.NewReader(file)
   135  	if err != nil {
   136  		return "", err
   137  	}
   138  	defer gr.Close()
   139  
   140  	bytes, err := io.ReadAll(gr)
   141  	if err != nil {
   142  		return "", err
   143  	}
   144  	return string(charset.MaybeRemoveBOM(bytes, charset.ConvertOpts{})), nil
   145  }
   146  
   147  func restoreOldDB(t *testing.T, version string) bool {
   148  	data, err := readSQLFromFile(version)
   149  	assert.NoError(t, err)
   150  	if len(data) == 0 {
   151  		tests.Printf("No db found to restore for %s version: %s\n", setting.Database.Type, version)
   152  		return false
   153  	}
   154  
   155  	switch {
   156  	case setting.Database.Type.IsSQLite3():
   157  		util.Remove(setting.Database.Path)
   158  		err := os.MkdirAll(path.Dir(setting.Database.Path), os.ModePerm)
   159  		assert.NoError(t, err)
   160  
   161  		db, err := sql.Open("sqlite3", fmt.Sprintf("file:%s?cache=shared&mode=rwc&_busy_timeout=%d&_txlock=immediate", setting.Database.Path, setting.Database.Timeout))
   162  		assert.NoError(t, err)
   163  		defer db.Close()
   164  
   165  		_, err = db.Exec(data)
   166  		assert.NoError(t, err)
   167  		db.Close()
   168  
   169  	case setting.Database.Type.IsMySQL():
   170  		db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s)/",
   171  			setting.Database.User, setting.Database.Passwd, setting.Database.Host))
   172  		assert.NoError(t, err)
   173  		defer db.Close()
   174  
   175  		_, err = db.Exec(fmt.Sprintf("DROP DATABASE IF EXISTS %s", setting.Database.Name))
   176  		assert.NoError(t, err)
   177  
   178  		_, err = db.Exec(fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s", setting.Database.Name))
   179  		assert.NoError(t, err)
   180  		db.Close()
   181  
   182  		db, err = sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s)/%s?multiStatements=true",
   183  			setting.Database.User, setting.Database.Passwd, setting.Database.Host, setting.Database.Name))
   184  		assert.NoError(t, err)
   185  		defer db.Close()
   186  
   187  		_, err = db.Exec(data)
   188  		assert.NoError(t, err)
   189  		db.Close()
   190  
   191  	case setting.Database.Type.IsPostgreSQL():
   192  		var db *sql.DB
   193  		var err error
   194  		if setting.Database.Host[0] == '/' {
   195  			db, err = sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@/?sslmode=%s&host=%s",
   196  				setting.Database.User, setting.Database.Passwd, setting.Database.SSLMode, setting.Database.Host))
   197  			assert.NoError(t, err)
   198  		} else {
   199  			db, err = sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@%s/?sslmode=%s",
   200  				setting.Database.User, setting.Database.Passwd, setting.Database.Host, setting.Database.SSLMode))
   201  			assert.NoError(t, err)
   202  		}
   203  		defer db.Close()
   204  
   205  		_, err = db.Exec(fmt.Sprintf("DROP DATABASE IF EXISTS %s", setting.Database.Name))
   206  		assert.NoError(t, err)
   207  
   208  		_, err = db.Exec(fmt.Sprintf("CREATE DATABASE %s", setting.Database.Name))
   209  		assert.NoError(t, err)
   210  		db.Close()
   211  
   212  		// Check if we need to setup a specific schema
   213  		if len(setting.Database.Schema) != 0 {
   214  			if setting.Database.Host[0] == '/' {
   215  				db, err = sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@/%s?sslmode=%s&host=%s",
   216  					setting.Database.User, setting.Database.Passwd, setting.Database.Name, setting.Database.SSLMode, setting.Database.Host))
   217  			} else {
   218  				db, err = sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=%s",
   219  					setting.Database.User, setting.Database.Passwd, setting.Database.Host, setting.Database.Name, setting.Database.SSLMode))
   220  			}
   221  			if !assert.NoError(t, err) {
   222  				return false
   223  			}
   224  			defer db.Close()
   225  
   226  			schrows, err := db.Query(fmt.Sprintf("SELECT 1 FROM information_schema.schemata WHERE schema_name = '%s'", setting.Database.Schema))
   227  			if !assert.NoError(t, err) || !assert.NotEmpty(t, schrows) {
   228  				return false
   229  			}
   230  
   231  			if !schrows.Next() {
   232  				// Create and setup a DB schema
   233  				_, err = db.Exec(fmt.Sprintf("CREATE SCHEMA %s", setting.Database.Schema))
   234  				assert.NoError(t, err)
   235  			}
   236  			schrows.Close()
   237  
   238  			// Make the user's default search path the created schema; this will affect new connections
   239  			_, err = db.Exec(fmt.Sprintf(`ALTER USER "%s" SET search_path = %s`, setting.Database.User, setting.Database.Schema))
   240  			assert.NoError(t, err)
   241  
   242  			db.Close()
   243  		}
   244  
   245  		if setting.Database.Host[0] == '/' {
   246  			db, err = sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@/%s?sslmode=%s&host=%s",
   247  				setting.Database.User, setting.Database.Passwd, setting.Database.Name, setting.Database.SSLMode, setting.Database.Host))
   248  		} else {
   249  			db, err = sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=%s",
   250  				setting.Database.User, setting.Database.Passwd, setting.Database.Host, setting.Database.Name, setting.Database.SSLMode))
   251  		}
   252  		assert.NoError(t, err)
   253  		defer db.Close()
   254  
   255  		_, err = db.Exec(data)
   256  		assert.NoError(t, err)
   257  		db.Close()
   258  
   259  	case setting.Database.Type.IsMSSQL():
   260  		host, port := setting.ParseMSSQLHostPort(setting.Database.Host)
   261  		db, err := sql.Open("mssql", fmt.Sprintf("server=%s; port=%s; database=%s; user id=%s; password=%s;",
   262  			host, port, "master", setting.Database.User, setting.Database.Passwd))
   263  		assert.NoError(t, err)
   264  		defer db.Close()
   265  
   266  		_, err = db.Exec("DROP DATABASE IF EXISTS [gitea]")
   267  		assert.NoError(t, err)
   268  
   269  		statements := strings.Split(data, "\nGO\n")
   270  		for _, statement := range statements {
   271  			if len(statement) > 5 && statement[:5] == "USE [" {
   272  				dbname := statement[5 : len(statement)-1]
   273  				db.Close()
   274  				db, err = sql.Open("mssql", fmt.Sprintf("server=%s; port=%s; database=%s; user id=%s; password=%s;",
   275  					host, port, dbname, setting.Database.User, setting.Database.Passwd))
   276  				assert.NoError(t, err)
   277  				defer db.Close()
   278  			}
   279  			_, err = db.Exec(statement)
   280  			assert.NoError(t, err, "Failure whilst running: %s\nError: %v", statement, err)
   281  		}
   282  		db.Close()
   283  	}
   284  	return true
   285  }
   286  
   287  func wrappedMigrate(x *xorm.Engine) error {
   288  	currentEngine = x
   289  	return migrations.Migrate(x)
   290  }
   291  
   292  func doMigrationTest(t *testing.T, version string) {
   293  	defer tests.PrintCurrentTest(t)()
   294  	tests.Printf("Performing migration test for %s version: %s\n", setting.Database.Type, version)
   295  	if !restoreOldDB(t, version) {
   296  		return
   297  	}
   298  
   299  	setting.InitSQLLoggersForCli(log.INFO)
   300  
   301  	err := db.InitEngineWithMigration(context.Background(), wrappedMigrate)
   302  	assert.NoError(t, err)
   303  	currentEngine.Close()
   304  
   305  	beans, _ := db.NamesToBean()
   306  
   307  	err = db.InitEngineWithMigration(context.Background(), func(x *xorm.Engine) error {
   308  		currentEngine = x
   309  		return migrate_base.RecreateTables(beans...)(x)
   310  	})
   311  	assert.NoError(t, err)
   312  	currentEngine.Close()
   313  
   314  	// We do this a second time to ensure that there is not a problem with retained indices
   315  	err = db.InitEngineWithMigration(context.Background(), func(x *xorm.Engine) error {
   316  		currentEngine = x
   317  		return migrate_base.RecreateTables(beans...)(x)
   318  	})
   319  	assert.NoError(t, err)
   320  
   321  	currentEngine.Close()
   322  }
   323  
   324  func TestMigrations(t *testing.T) {
   325  	defer initMigrationTest(t)()
   326  
   327  	dialect := setting.Database.Type
   328  	versions, err := availableVersions()
   329  	assert.NoError(t, err)
   330  
   331  	if len(versions) == 0 {
   332  		tests.Printf("No old database versions available to migration test for %s\n", dialect)
   333  		return
   334  	}
   335  
   336  	tests.Printf("Preparing to test %d migrations for %s\n", len(versions), dialect)
   337  	for _, version := range versions {
   338  		t.Run(fmt.Sprintf("Migrate-%s-%s", dialect, version), func(t *testing.T) {
   339  			doMigrationTest(t, version)
   340  		})
   341  	}
   342  }