From deb31d3f3030d2a6aad2f966a9efc365f49daf67 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Fri, 1 May 2026 23:38:38 +0800 Subject: [PATCH] Refactor database connection (#37496) Clean up legacy copied&pasted code, introduce the unique "database connection" function. Move migration testing helper function PrepareTestEnv to a separate package. By the way, remove "shadow connection secrets" tricks: showing connection string on UI is useless --------- Co-authored-by: Nicolas --- Makefile | 2 +- cmd/dump.go | 4 +- models/db/conn.go | 173 ++++++++++++++ .../db/conn_test.go | 10 +- ...ith_schema.go => driver_postgresschema.go} | 4 +- models/db/driver_sqlite_mattn.go | 34 +++ models/db/engine_dump.go | 8 +- models/db/engine_init.go | 28 +-- models/db/engine_test.go | 2 +- models/migrations/base/db_test.go | 5 +- models/migrations/base/tests.go | 223 ------------------ models/migrations/migrationtest/tests.go | 120 ++++++++++ models/migrations/v1_14/main_test.go | 4 +- models/migrations/v1_14/v176_test.go | 4 +- models/migrations/v1_14/v177_test.go | 4 +- models/migrations/v1_15/main_test.go | 4 +- models/migrations/v1_15/v181_test.go | 4 +- models/migrations/v1_15/v182_test.go | 4 +- models/migrations/v1_16/main_test.go | 4 +- models/migrations/v1_16/v189_test.go | 4 +- models/migrations/v1_16/v193_test.go | 4 +- models/migrations/v1_16/v195_test.go | 4 +- models/migrations/v1_16/v210_test.go | 4 +- models/migrations/v1_17/main_test.go | 4 +- models/migrations/v1_17/v221_test.go | 4 +- models/migrations/v1_18/main_test.go | 4 +- models/migrations/v1_18/v229_test.go | 4 +- models/migrations/v1_18/v230_test.go | 4 +- models/migrations/v1_19/main_test.go | 4 +- models/migrations/v1_19/v233_test.go | 4 +- models/migrations/v1_20/main_test.go | 4 +- models/migrations/v1_20/v259_test.go | 4 +- models/migrations/v1_21/main_test.go | 4 +- models/migrations/v1_22/main_test.go | 4 +- models/migrations/v1_22/v283_test.go | 4 +- models/migrations/v1_22/v286_test.go | 4 +- models/migrations/v1_22/v287_test.go | 4 +- models/migrations/v1_22/v293_test.go | 4 +- models/migrations/v1_22/v294_test.go | 4 +- models/migrations/v1_23/main_test.go | 4 +- models/migrations/v1_23/v302_test.go | 4 +- models/migrations/v1_23/v304_test.go | 4 +- models/migrations/v1_25/main_test.go | 4 +- models/migrations/v1_25/v321_test.go | 6 +- models/migrations/v1_25/v322_test.go | 6 +- models/migrations/v1_26/main_test.go | 4 +- models/migrations/v1_26/v325_test.go | 4 +- models/migrations/v1_26/v326_test.go | 4 +- models/migrations/v1_26/v327_test.go | 4 +- models/migrations/v1_26/v329_test.go | 4 +- models/migrations/v1_27/main_test.go | 4 +- models/migrations/v1_27/v331_test.go | 6 +- models/unittest/testdb.go | 106 ++++++++- modules/setting/database.go | 157 ++---------- modules/setting/database_sqlite.go | 15 -- options/locale/locale_en-US.json | 2 - routers/init.go | 6 - routers/install/install.go | 4 +- routers/web/admin/admin_test.go | 58 ----- routers/web/admin/config.go | 63 +---- routers/web/healthcheck/check.go | 10 +- templates/admin/config.tmpl | 4 - .../migration-test/gitea-v1.6.4.mssql.sql.gz | Bin 12969 -> 13260 bytes .../migration-test/gitea-v1.7.0.mssql.sql.gz | Bin 13068 -> 13369 bytes .../migration-test/migration_test.go | 174 ++++---------- tests/sqlite.ini.tmpl | 2 +- tests/test_utils.go | 96 +------- 67 files changed, 611 insertions(+), 865 deletions(-) create mode 100644 models/db/conn.go rename modules/setting/database_test.go => models/db/conn_test.go (88%) rename models/db/{sql_postgres_with_schema.go => driver_postgresschema.go} (92%) create mode 100644 models/db/driver_sqlite_mattn.go delete mode 100644 models/migrations/base/tests.go create mode 100644 models/migrations/migrationtest/tests.go delete mode 100644 modules/setting/database_sqlite.go diff --git a/Makefile b/Makefile index ca4df2b174..ad7739c07b 100644 --- a/Makefile +++ b/Makefile @@ -164,7 +164,7 @@ TEST_PGSQL_PASSWORD ?= postgres TEST_PGSQL_SCHEMA ?= gtestschema TEST_MINIO_ENDPOINT ?= minio:9000 TEST_MSSQL_HOST ?= mssql:1433 -TEST_MSSQL_DBNAME ?= gitea +TEST_MSSQL_DBNAME ?= testgitea TEST_MSSQL_USERNAME ?= sa TEST_MSSQL_PASSWORD ?= MwantsaSecurePassword1 diff --git a/cmd/dump.go b/cmd/dump.go index 49f4d9e894..40b73f69aa 100644 --- a/cmd/dump.go +++ b/cmd/dump.go @@ -203,8 +203,8 @@ func runDump(ctx context.Context, cmd *cli.Command) error { } }() - targetDBType := cmd.String("database") - if len(targetDBType) > 0 && targetDBType != setting.Database.Type.String() { + targetDBType := setting.DatabaseType(cmd.String("database")) + if targetDBType != "" && targetDBType != setting.Database.Type { log.Info("Dumping database %s => %s...", setting.Database.Type, targetDBType) } else { log.Info("Dumping database...") diff --git a/models/db/conn.go b/models/db/conn.go new file mode 100644 index 0000000000..de6f3cd5ec --- /dev/null +++ b/models/db/conn.go @@ -0,0 +1,173 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package db + +import ( + "errors" + "fmt" + "net" + "net/url" + "os" + "path/filepath" + "strings" + + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" +) + +type ConnOptions struct { + Type setting.DatabaseType + Host string + Database string + User string + Passwd string + Schema string + SSLMode string + + SQLitePath string + SQLiteBusyTimeout int + SQLiteJournalMode string +} + +type SQLiteConnStrOptions struct { + FilePath string + BusyTimeout int + JournalMode string +} + +func GlobalConnOptions() ConnOptions { + return ConnOptions{ + Type: setting.Database.Type, + Host: setting.Database.Host, + Database: setting.Database.Name, + User: setting.Database.User, + Passwd: setting.Database.Passwd, + Schema: setting.Database.Schema, + SSLMode: setting.Database.SSLMode, + + SQLitePath: setting.Database.Path, + SQLiteBusyTimeout: setting.Database.SQLiteBusyTimeout, + SQLiteJournalMode: setting.Database.SQLiteJournalMode, + } +} + +const sqlDriverPostgresSchema = "postgresschema" + +var makeSQLiteConnStr = func(opts SQLiteConnStrOptions) (string, string, error) { + return "", "", errors.New(`this Gitea binary was not built with SQLite3 support, get an official release or rebuild with: -tags sqlite,sqlite_unlock_notify`) +} + +func ConnStrDefaultDatabase(opts ConnOptions) (string, string, error) { + opts.Database, opts.Schema = "", "" + return ConnStr(opts) +} + +func ConnStr(opts ConnOptions) (string, string, error) { + switch { + case opts.Type.IsMySQL(): + // use unix socket or tcp socket + connType := util.Iif(strings.HasPrefix(opts.Host, "/"), "unix", "tcp") + // allow (Postgres-inspired) default value to work in MySQL + tls := util.Iif(opts.SSLMode == "disable", "false", opts.SSLMode) + // in case the database name is a partial connection string which contains "?" parameters + paramSep := util.Iif(strings.Contains(opts.Database, "?"), "&", "?") + connStr := fmt.Sprintf("%s:%s@%s(%s)/%s%sparseTime=true&tls=%s", opts.User, opts.Passwd, connType, opts.Host, opts.Database, paramSep, tls) + return "mysql", connStr, nil + + case opts.Type.IsPostgreSQL(): + connStr := makePgSQLConnStr(opts.Host, opts.User, opts.Passwd, opts.Database, opts.SSLMode) + driver := util.Iif(opts.Schema == "", "postgres", sqlDriverPostgresSchema) + registerPostgresSchemaDriver() + return driver, connStr, nil + + case opts.Type.IsMSSQL(): + host, port := parseMSSQLHostPort(opts.Host) + connStr := fmt.Sprintf("server=%s; port=%s; user id=%s; password=%s;", host, port, opts.User, opts.Passwd) + if opts.Database != "" { + connStr += "; database=" + opts.Database + } + return "mssql", connStr, nil + + case opts.Type.IsSQLite3(): + if opts.SQLitePath == "" { + return "", "", errors.New("sqlite3 database path cannot be empty") + } + if err := os.MkdirAll(filepath.Dir(opts.SQLitePath), os.ModePerm); err != nil { + return "", "", fmt.Errorf("failed to create directories: %w", err) + } + return makeSQLiteConnStr(SQLiteConnStrOptions{ + FilePath: opts.SQLitePath, + JournalMode: opts.SQLiteJournalMode, + BusyTimeout: opts.SQLiteBusyTimeout, + }) + } + return "", "", fmt.Errorf("unknown database type: %s", opts.Type) +} + +// parsePgSQLHostPort parses given input in various forms defined in +// https://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-CONNSTRING +// and returns proper host and port number. +func parsePgSQLHostPort(info string) (host, port string) { + if h, p, err := net.SplitHostPort(info); err == nil { + host, port = h, p + } else { + // treat the "info" as "host", if it's an IPv6 address, remove the wrapper + host = info + if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") { + host = host[1 : len(host)-1] + } + } + + // set fallback values + if host == "" { + host = "127.0.0.1" + } + if port == "" { + port = "5432" + } + return host, port +} + +func makePgSQLConnStr(dbHost, dbUser, dbPasswd, dbName, dbsslMode string) (connStr string) { + dbName, dbParam, _ := strings.Cut(dbName, "?") + host, port := parsePgSQLHostPort(dbHost) + connURL := url.URL{ + Scheme: "postgres", + User: url.UserPassword(dbUser, dbPasswd), + Host: net.JoinHostPort(host, port), + Path: dbName, + OmitHost: false, + RawQuery: dbParam, + } + query := connURL.Query() + if strings.HasPrefix(host, "/") { // looks like a unix socket + query.Add("host", host) + connURL.Host = ":" + port + } + query.Set("sslmode", dbsslMode) + connURL.RawQuery = query.Encode() + return connURL.String() +} + +// parseMSSQLHostPort splits the host into host and port +func parseMSSQLHostPort(info string) (string, string) { + // the default port "0" might be related to MSSQL's dynamic port, maybe it should be double-confirmed in the future + host, port := "127.0.0.1", "0" + if strings.Contains(info, ":") { + host = strings.Split(info, ":")[0] + port = strings.Split(info, ":")[1] + } else if strings.Contains(info, ",") { + host = strings.Split(info, ",")[0] + port = strings.TrimSpace(strings.Split(info, ",")[1]) + } else if len(info) > 0 { + host = info + } + if host == "" { + host = "127.0.0.1" + } + if port == "" { + port = "0" + } + return host, port +} diff --git a/modules/setting/database_test.go b/models/db/conn_test.go similarity index 88% rename from modules/setting/database_test.go rename to models/db/conn_test.go index a742d54f8c..ba33d252f2 100644 --- a/modules/setting/database_test.go +++ b/models/db/conn_test.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package setting +package db import ( "testing" @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/assert" ) -func Test_parsePostgreSQLHostPort(t *testing.T) { +func TestParsePgSQLHostPort(t *testing.T) { tests := map[string]struct { HostPort string Host string @@ -49,14 +49,14 @@ func Test_parsePostgreSQLHostPort(t *testing.T) { for k, test := range tests { t.Run(k, func(t *testing.T) { t.Log(test.HostPort) - host, port := parsePostgreSQLHostPort(test.HostPort) + host, port := parsePgSQLHostPort(test.HostPort) assert.Equal(t, test.Host, host) assert.Equal(t, test.Port, port) }) } } -func Test_getPostgreSQLConnectionString(t *testing.T) { +func TestMakePgSQLConnStr(t *testing.T) { tests := []struct { Host string User string @@ -103,7 +103,7 @@ func Test_getPostgreSQLConnectionString(t *testing.T) { } for _, test := range tests { - connStr := getPostgreSQLConnectionString(test.Host, test.User, test.Passwd, test.Name, test.SSLMode) + connStr := makePgSQLConnStr(test.Host, test.User, test.Passwd, test.Name, test.SSLMode) assert.Equal(t, test.Output, connStr) } } diff --git a/models/db/sql_postgres_with_schema.go b/models/db/driver_postgresschema.go similarity index 92% rename from models/db/sql_postgres_with_schema.go rename to models/db/driver_postgresschema.go index 812fe4a6a6..b673500763 100644 --- a/models/db/sql_postgres_with_schema.go +++ b/models/db/driver_postgresschema.go @@ -18,8 +18,8 @@ var registerOnce sync.Once func registerPostgresSchemaDriver() { registerOnce.Do(func() { - sql.Register("postgresschema", &postgresSchemaDriver{}) - dialects.RegisterDriver("postgresschema", dialects.QueryDriver("postgres")) + sql.Register(sqlDriverPostgresSchema, &postgresSchemaDriver{}) + dialects.RegisterDriver(sqlDriverPostgresSchema, dialects.QueryDriver("postgres")) }) } diff --git a/models/db/driver_sqlite_mattn.go b/models/db/driver_sqlite_mattn.go new file mode 100644 index 0000000000..4988a43d3f --- /dev/null +++ b/models/db/driver_sqlite_mattn.go @@ -0,0 +1,34 @@ +//go:build sqlite + +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package db + +import ( + "fmt" + "strconv" + "strings" + + "code.gitea.io/gitea/modules/setting" + + _ "github.com/mattn/go-sqlite3" +) + +func init() { + setting.SupportedDatabaseTypes = append(setting.SupportedDatabaseTypes, "sqlite3") + makeSQLiteConnStr = makeSQLiteConnStrMattnCGO +} + +func makeSQLiteConnStrMattnCGO(opts SQLiteConnStrOptions) (string, string, error) { + var params []string + params = append(params, "cache=shared") + params = append(params, "mode=rwc") + params = append(params, "_busy_timeout="+strconv.Itoa(opts.BusyTimeout)) + params = append(params, "_txlock=immediate") + if opts.JournalMode != "" { + params = append(params, "_journal_mode="+opts.JournalMode) + } + connStr := fmt.Sprintf("file:%s?%s", opts.FilePath, strings.Join(params, "&")) + return "sqlite3", connStr, nil +} diff --git a/models/db/engine_dump.go b/models/db/engine_dump.go index 63f2d4e093..1d8d555b44 100644 --- a/models/db/engine_dump.go +++ b/models/db/engine_dump.go @@ -3,10 +3,14 @@ package db -import "xorm.io/xorm/schemas" +import ( + "code.gitea.io/gitea/modules/setting" + + "xorm.io/xorm/schemas" +) // DumpDatabase dumps all data from database according the special database SQL syntax to file system. -func DumpDatabase(filePath, dbType string) error { +func DumpDatabase(filePath string, dbType setting.DatabaseType) error { var tbs []*schemas.Table for _, t := range registeredModels { t, err := xormEngine.TableInfo(t) diff --git a/models/db/engine_init.go b/models/db/engine_init.go index ef5db3ff5e..65192d3327 100644 --- a/models/db/engine_init.go +++ b/models/db/engine_init.go @@ -6,7 +6,6 @@ package db import ( "context" "fmt" - "strings" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" @@ -24,31 +23,23 @@ func init() { // newXORMEngine returns a new XORM engine from the configuration func newXORMEngine() (*xorm.Engine, error) { - connStr, err := setting.DBConnStr() + connOpts := GlobalConnOptions() + driver, connStr, err := ConnStr(connOpts) if err != nil { return nil, err } - var engine *xorm.Engine - - if setting.Database.Type.IsPostgreSQL() && len(setting.Database.Schema) > 0 { - // OK whilst we sort out our schema issues - create a schema aware postgres - registerPostgresSchemaDriver() - engine, err = xorm.NewEngine("postgresschema", connStr) - } else { - engine, err = xorm.NewEngine(setting.Database.Type.String(), connStr) - } - + engine, err := xorm.NewEngine(driver, connStr) if err != nil { return nil, err } - switch setting.Database.Type { - case "mysql": + switch { + case connOpts.Type.IsMySQL(): engine.Dialect().SetParams(map[string]string{"rowFormat": "DYNAMIC"}) - case "mssql": + case connOpts.Type.IsMSSQL(): engine.Dialect().SetParams(map[string]string{"DEFAULT_VARCHAR": "nvarchar"}) } - engine.SetSchema(setting.Database.Schema) + engine.SetSchema(connOpts.Schema) return engine, nil } @@ -56,10 +47,7 @@ func newXORMEngine() (*xorm.Engine, error) { func InitEngine(ctx context.Context) error { xe, err := newXORMEngine() if err != nil { - if strings.Contains(err.Error(), "SQLite3 support") { - return fmt.Errorf("sqlite3 requires: -tags sqlite,sqlite_unlock_notify\n%w", err) - } - return fmt.Errorf("failed to connect to database: %w", err) + return fmt.Errorf("failed to init database engine: %w", err) } xe.SetMapper(names.GonicMapper{}) diff --git a/models/db/engine_test.go b/models/db/engine_test.go index 1c218df77f..6a6264b535 100644 --- a/models/db/engine_test.go +++ b/models/db/engine_test.go @@ -30,7 +30,7 @@ func TestDumpDatabase(t *testing.T) { assert.NoError(t, db.GetEngine(t.Context()).Sync(new(Version))) for _, dbType := range setting.SupportedDatabaseTypes { - assert.NoError(t, db.DumpDatabase(filepath.Join(dir, dbType+".sql"), dbType)) + assert.NoError(t, db.DumpDatabase(filepath.Join(dir, dbType+".sql"), setting.DatabaseType(dbType))) } } diff --git a/models/migrations/base/db_test.go b/models/migrations/base/db_test.go index dc4e502e42..ce6e1169d8 100644 --- a/models/migrations/base/db_test.go +++ b/models/migrations/base/db_test.go @@ -6,17 +6,18 @@ package base import ( "testing" + "code.gitea.io/gitea/models/migrations/migrationtest" "code.gitea.io/gitea/modules/timeutil" "xorm.io/xorm/names" ) func TestMain(m *testing.M) { - MainTest(m) + migrationtest.MainTest(m) } func Test_DropTableColumns(t *testing.T) { - x, deferable := PrepareTestEnv(t, 0) + x, deferable := migrationtest.PrepareTestEnv(t, 0) defer deferable() // FIXME: this logic seems wrong. Need to add an assertion here in the future, but it seems causing failure. if x == nil || t.Failed() { diff --git a/models/migrations/base/tests.go b/models/migrations/base/tests.go deleted file mode 100644 index 0ec979a513..0000000000 --- a/models/migrations/base/tests.go +++ /dev/null @@ -1,223 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package base - -import ( - "database/sql" - "fmt" - "os" - "path" - "path/filepath" - "testing" - - "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/models/unittest" - "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/testlogger" - "code.gitea.io/gitea/modules/util" - - "github.com/stretchr/testify/require" - "xorm.io/xorm" - "xorm.io/xorm/schemas" -) - -// FIXME: this file shouldn't be in a normal package, it should only be compiled for tests - -func newXORMEngine(t *testing.T) (*xorm.Engine, error) { - if err := db.InitEngine(t.Context()); err != nil { - return nil, err - } - x := unittest.GetXORMEngine() - return x, nil -} - -func deleteDB() error { - switch { - case setting.Database.Type.IsSQLite3(): - if err := util.Remove(setting.Database.Path); err != nil { - return err - } - return os.MkdirAll(path.Dir(setting.Database.Path), os.ModePerm) - - case setting.Database.Type.IsMySQL(): - db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s)/", - setting.Database.User, setting.Database.Passwd, setting.Database.Host)) - if err != nil { - return err - } - defer db.Close() - - if _, err = db.Exec("DROP DATABASE IF EXISTS " + setting.Database.Name); err != nil { - return err - } - - if _, err = db.Exec("CREATE DATABASE IF NOT EXISTS " + setting.Database.Name); err != nil { - return err - } - return nil - case setting.Database.Type.IsPostgreSQL(): - db, err := sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@%s/?sslmode=%s", - setting.Database.User, setting.Database.Passwd, setting.Database.Host, setting.Database.SSLMode)) - if err != nil { - return err - } - defer db.Close() - - if _, err = db.Exec("DROP DATABASE IF EXISTS " + setting.Database.Name); err != nil { - return err - } - - if _, err = db.Exec("CREATE DATABASE " + setting.Database.Name); err != nil { - return err - } - db.Close() - - // Check if we need to set up a specific schema - if len(setting.Database.Schema) != 0 { - db, err = sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=%s", - setting.Database.User, setting.Database.Passwd, setting.Database.Host, setting.Database.Name, setting.Database.SSLMode)) - if err != nil { - return err - } - defer db.Close() - - schrows, err := db.Query(fmt.Sprintf("SELECT 1 FROM information_schema.schemata WHERE schema_name = '%s'", setting.Database.Schema)) - if err != nil { - return err - } - defer schrows.Close() - - if !schrows.Next() { - // Create and set up a DB schema - _, err = db.Exec("CREATE SCHEMA " + setting.Database.Schema) - if err != nil { - return err - } - } - - // Make the user's default search path the created schema; this will affect new connections - _, err = db.Exec(fmt.Sprintf(`ALTER USER "%s" SET search_path = %s`, setting.Database.User, setting.Database.Schema)) - if err != nil { - return err - } - return nil - } - case setting.Database.Type.IsMSSQL(): - host, port := setting.ParseMSSQLHostPort(setting.Database.Host) - db, err := sql.Open("mssql", fmt.Sprintf("server=%s; port=%s; database=%s; user id=%s; password=%s;", - host, port, "master", setting.Database.User, setting.Database.Passwd)) - if err != nil { - return err - } - defer db.Close() - - if _, err = db.Exec(fmt.Sprintf("DROP DATABASE IF EXISTS [%s]", setting.Database.Name)); err != nil { - return err - } - if _, err = db.Exec(fmt.Sprintf("CREATE DATABASE [%s]", setting.Database.Name)); err != nil { - return err - } - default: - return fmt.Errorf("unsupported database type: %s", setting.Database.Type) - } - - return nil -} - -// PrepareTestEnv prepares the test environment and reset the database. The skip parameter should usually be 0. -// Provide models to be sync'd with the database - in particular any models you expect fixtures to be loaded from. -// -// fixtures in `models/migrations/fixtures/` will be loaded automatically -func PrepareTestEnv(t *testing.T, skip int, syncModels ...any) (*xorm.Engine, func()) { - t.Helper() - ourSkip := 2 - ourSkip += skip - deferFn := testlogger.PrintCurrentTest(t, ourSkip) - giteaRoot := setting.GetGiteaTestSourceRoot() - require.NoError(t, unittest.SyncDirs(filepath.Join(giteaRoot, "tests/gitea-repositories-meta"), setting.RepoRootPath)) - - if err := deleteDB(); err != nil { - t.Fatalf("unable to reset database: %v", err) - return nil, deferFn - } - - x, err := newXORMEngine(t) - require.NoError(t, err) - if x != nil { - oldDefer := deferFn - deferFn = func() { - oldDefer() - if err := x.Close(); err != nil { - t.Errorf("error during close: %v", err) - } - if err := deleteDB(); err != nil { - t.Errorf("unable to reset database: %v", err) - } - } - } - if err != nil { - return x, deferFn - } - - if len(syncModels) > 0 { - if err := x.Sync(syncModels...); err != nil { - t.Errorf("error during sync: %v", err) - return x, deferFn - } - } - - fixturesDir := filepath.Join(giteaRoot, "models", "migrations", "fixtures", t.Name()) - - if _, err := os.Stat(fixturesDir); err == nil { - t.Logf("initializing fixtures from: %s", fixturesDir) - if err := unittest.InitFixtures( - unittest.FixturesOptions{ - Dir: fixturesDir, - }, x); err != nil { - t.Errorf("error whilst initializing fixtures from %s: %v", fixturesDir, err) - return x, deferFn - } - if err := unittest.LoadFixtures(); err != nil { - t.Errorf("error whilst loading fixtures from %s: %v", fixturesDir, err) - return x, deferFn - } - } else if !os.IsNotExist(err) { - t.Errorf("unexpected error whilst checking for existence of fixtures: %v", err) - } else { - t.Logf("no fixtures found in: %s", fixturesDir) - } - - return x, deferFn -} - -func LoadTableSchemasMap(t *testing.T, x *xorm.Engine) map[string]*schemas.Table { - tables, err := x.DBMetas() - require.NoError(t, err) - tableMap := make(map[string]*schemas.Table) - for _, table := range tables { - tableMap[table.Name] = table - } - return tableMap -} - -func mainTest(m *testing.M) int { - testlogger.Init() - err := setting.PrepareIntegrationTestConfig() - if err != nil { - return testlogger.MainErrorf("Unable to prepare integration test config: %v", err) - } - setting.SetupGiteaTestEnv() - - if err = git.InitFull(); err != nil { - return testlogger.MainErrorf("Unable to InitFull: %v", err) - } - setting.LoadDBSetting() - setting.InitLoggersForTest() - return m.Run() -} - -func MainTest(m *testing.M) { - os.Exit(mainTest(m)) -} diff --git a/models/migrations/migrationtest/tests.go b/models/migrations/migrationtest/tests.go new file mode 100644 index 0000000000..ed8bb16ef1 --- /dev/null +++ b/models/migrations/migrationtest/tests.go @@ -0,0 +1,120 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package migrationtest + +import ( + "os" + "path/filepath" + "testing" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/testlogger" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "xorm.io/xorm" + "xorm.io/xorm/schemas" +) + +// PrepareTestEnv prepares the test environment and reset the database. The skip parameter should usually be 0. +// Provide models to be sync'd with the database - in particular any models you expect fixtures to be loaded from. +// +// fixtures in `models/migrations/fixtures/` will be loaded automatically +func PrepareTestEnv(t *testing.T, skip int, syncModels ...any) (*xorm.Engine, func()) { + t.Helper() + ourSkip := 2 + ourSkip += skip + deferFn := testlogger.PrintCurrentTest(t, ourSkip) + giteaRoot := setting.GetGiteaTestSourceRoot() + require.NoError(t, unittest.SyncDirs(filepath.Join(giteaRoot, "tests/gitea-repositories-meta"), setting.RepoRootPath)) + + cleanup, err := unittest.ResetTestDatabase() + if err != nil { + t.Fatalf("unable to reset database: %v", err) + return nil, deferFn + } + { + oldDefer := deferFn + deferFn = func() { + cleanup() + oldDefer() + } + } + + err = db.InitEngine(t.Context()) + if !assert.NoError(t, err) { + return nil, deferFn + } + x := unittest.GetXORMEngine() + { + oldDefer := deferFn + deferFn = func() { + _ = x.Close() + oldDefer() + } + } + + if len(syncModels) > 0 { + if err := x.Sync(syncModels...); err != nil { + t.Errorf("error during sync: %v", err) + return x, deferFn + } + } + + fixturesDir := filepath.Join(giteaRoot, "models", "migrations", "fixtures", t.Name()) + + if _, err := os.Stat(fixturesDir); err == nil { + t.Logf("initializing fixtures from: %s", fixturesDir) + if err := unittest.InitFixtures( + unittest.FixturesOptions{ + Dir: fixturesDir, + }, x); err != nil { + t.Errorf("error whilst initializing fixtures from %s: %v", fixturesDir, err) + return x, deferFn + } + if err := unittest.LoadFixtures(); err != nil { + t.Errorf("error whilst loading fixtures from %s: %v", fixturesDir, err) + return x, deferFn + } + } else if !os.IsNotExist(err) { + t.Errorf("unexpected error whilst checking for existence of fixtures: %v", err) + } else { + t.Logf("no fixtures found in: %s", fixturesDir) + } + + return x, deferFn +} + +func LoadTableSchemasMap(t *testing.T, x *xorm.Engine) map[string]*schemas.Table { + tables, err := x.DBMetas() + require.NoError(t, err) + tableMap := make(map[string]*schemas.Table) + for _, table := range tables { + tableMap[table.Name] = table + } + return tableMap +} + +func mainTest(m *testing.M) int { + testlogger.Init() + err := setting.PrepareIntegrationTestConfig() + if err != nil { + return testlogger.MainErrorf("Unable to prepare integration test config: %v", err) + } + setting.SetupGiteaTestEnv() + + if err = git.InitFull(); err != nil { + return testlogger.MainErrorf("Unable to InitFull: %v", err) + } + setting.LoadDBSetting() + setting.InitLoggersForTest() + return m.Run() +} + +func MainTest(m *testing.M) { + os.Exit(mainTest(m)) +} diff --git a/models/migrations/v1_14/main_test.go b/models/migrations/v1_14/main_test.go index 978f88577c..6ed240c407 100644 --- a/models/migrations/v1_14/main_test.go +++ b/models/migrations/v1_14/main_test.go @@ -6,9 +6,9 @@ package v1_14 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" ) func TestMain(m *testing.M) { - base.MainTest(m) + migrationtest.MainTest(m) } diff --git a/models/migrations/v1_14/v176_test.go b/models/migrations/v1_14/v176_test.go index 5c1db4db71..aa57b5ad1d 100644 --- a/models/migrations/v1_14/v176_test.go +++ b/models/migrations/v1_14/v176_test.go @@ -6,7 +6,7 @@ package v1_14 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "github.com/stretchr/testify/assert" ) @@ -47,7 +47,7 @@ func Test_RemoveInvalidLabels(t *testing.T) { } // load and prepare the test database - x, deferable := base.PrepareTestEnv(t, 0, new(Comment), new(Issue), new(Repository), new(IssueLabel), new(Label)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(Comment), new(Issue), new(Repository), new(IssueLabel), new(Label)) if x == nil || t.Failed() { defer deferable() return diff --git a/models/migrations/v1_14/v177_test.go b/models/migrations/v1_14/v177_test.go index 263f69f338..a86fb98830 100644 --- a/models/migrations/v1_14/v177_test.go +++ b/models/migrations/v1_14/v177_test.go @@ -6,7 +6,7 @@ package v1_14 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "code.gitea.io/gitea/modules/timeutil" "github.com/stretchr/testify/assert" @@ -34,7 +34,7 @@ func Test_DeleteOrphanedIssueLabels(t *testing.T) { } // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(IssueLabel), new(Label)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(IssueLabel), new(Label)) if x == nil || t.Failed() { defer deferable() return diff --git a/models/migrations/v1_15/main_test.go b/models/migrations/v1_15/main_test.go index d01585e997..768bbd310b 100644 --- a/models/migrations/v1_15/main_test.go +++ b/models/migrations/v1_15/main_test.go @@ -6,9 +6,9 @@ package v1_15 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" ) func TestMain(m *testing.M) { - base.MainTest(m) + migrationtest.MainTest(m) } diff --git a/models/migrations/v1_15/v181_test.go b/models/migrations/v1_15/v181_test.go index 73b5c1f3d6..e230c684ea 100644 --- a/models/migrations/v1_15/v181_test.go +++ b/models/migrations/v1_15/v181_test.go @@ -7,7 +7,7 @@ import ( "strings" "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "github.com/stretchr/testify/assert" ) @@ -20,7 +20,7 @@ func Test_AddPrimaryEmail2EmailAddress(t *testing.T) { } // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(User)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(User)) if x == nil || t.Failed() { defer deferable() return diff --git a/models/migrations/v1_15/v182_test.go b/models/migrations/v1_15/v182_test.go index 5fc6a0c467..c0a1378534 100644 --- a/models/migrations/v1_15/v182_test.go +++ b/models/migrations/v1_15/v182_test.go @@ -6,7 +6,7 @@ package v1_15 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "github.com/stretchr/testify/assert" ) @@ -20,7 +20,7 @@ func Test_AddIssueResourceIndexTable(t *testing.T) { } // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(Issue)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(Issue)) if x == nil || t.Failed() { defer deferable() return diff --git a/models/migrations/v1_16/main_test.go b/models/migrations/v1_16/main_test.go index 7f93d6e9e5..c54424788d 100644 --- a/models/migrations/v1_16/main_test.go +++ b/models/migrations/v1_16/main_test.go @@ -6,9 +6,9 @@ package v1_16 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" ) func TestMain(m *testing.M) { - base.MainTest(m) + migrationtest.MainTest(m) } diff --git a/models/migrations/v1_16/v189_test.go b/models/migrations/v1_16/v189_test.go index fb56ac8e11..44424dd369 100644 --- a/models/migrations/v1_16/v189_test.go +++ b/models/migrations/v1_16/v189_test.go @@ -6,7 +6,7 @@ package v1_16 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "code.gitea.io/gitea/modules/json" "github.com/stretchr/testify/assert" @@ -27,7 +27,7 @@ func (ls *LoginSourceOriginalV189) TableName() string { func Test_UnwrapLDAPSourceCfg(t *testing.T) { // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(LoginSourceOriginalV189)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(LoginSourceOriginalV189)) if x == nil || t.Failed() { defer deferable() return diff --git a/models/migrations/v1_16/v193_test.go b/models/migrations/v1_16/v193_test.go index 2e827f0550..f68dd6d92d 100644 --- a/models/migrations/v1_16/v193_test.go +++ b/models/migrations/v1_16/v193_test.go @@ -6,7 +6,7 @@ package v1_16 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "github.com/stretchr/testify/assert" ) @@ -31,7 +31,7 @@ func Test_AddRepoIDForAttachment(t *testing.T) { } // Prepare and load the testing database - x, deferrable := base.PrepareTestEnv(t, 0, new(Attachment), new(Issue), new(Release)) + x, deferrable := migrationtest.PrepareTestEnv(t, 0, new(Attachment), new(Issue), new(Release)) defer deferrable() if x == nil || t.Failed() { return diff --git a/models/migrations/v1_16/v195_test.go b/models/migrations/v1_16/v195_test.go index 946e06e399..bbfa5e162a 100644 --- a/models/migrations/v1_16/v195_test.go +++ b/models/migrations/v1_16/v195_test.go @@ -6,7 +6,7 @@ package v1_16 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "github.com/stretchr/testify/assert" ) @@ -21,7 +21,7 @@ func Test_AddTableCommitStatusIndex(t *testing.T) { } // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(CommitStatus)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(CommitStatus)) if x == nil || t.Failed() { defer deferable() return diff --git a/models/migrations/v1_16/v210_test.go b/models/migrations/v1_16/v210_test.go index 3b4ac7aa4b..7bff2572e1 100644 --- a/models/migrations/v1_16/v210_test.go +++ b/models/migrations/v1_16/v210_test.go @@ -6,7 +6,7 @@ package v1_16 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "code.gitea.io/gitea/modules/timeutil" "github.com/stretchr/testify/assert" @@ -44,7 +44,7 @@ func Test_RemigrateU2FCredentials(t *testing.T) { } // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(WebauthnCredential), new(U2fRegistration), new(ExpectedWebauthnCredential)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(WebauthnCredential), new(U2fRegistration), new(ExpectedWebauthnCredential)) if x == nil || t.Failed() { defer deferable() return diff --git a/models/migrations/v1_17/main_test.go b/models/migrations/v1_17/main_test.go index 571a4f55a3..8652201871 100644 --- a/models/migrations/v1_17/main_test.go +++ b/models/migrations/v1_17/main_test.go @@ -6,9 +6,9 @@ package v1_17 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" ) func TestMain(m *testing.M) { - base.MainTest(m) + migrationtest.MainTest(m) } diff --git a/models/migrations/v1_17/v221_test.go b/models/migrations/v1_17/v221_test.go index a2dc0fae55..6fda9b9980 100644 --- a/models/migrations/v1_17/v221_test.go +++ b/models/migrations/v1_17/v221_test.go @@ -7,7 +7,7 @@ import ( "encoding/base32" "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "github.com/stretchr/testify/assert" ) @@ -38,7 +38,7 @@ func Test_StoreWebauthnCredentialIDAsBytes(t *testing.T) { } // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(WebauthnCredential), new(ExpectedWebauthnCredential)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(WebauthnCredential), new(ExpectedWebauthnCredential)) defer deferable() if x == nil || t.Failed() { return diff --git a/models/migrations/v1_18/main_test.go b/models/migrations/v1_18/main_test.go index ebcfb45a94..b8641526f3 100644 --- a/models/migrations/v1_18/main_test.go +++ b/models/migrations/v1_18/main_test.go @@ -6,9 +6,9 @@ package v1_18 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" ) func TestMain(m *testing.M) { - base.MainTest(m) + migrationtest.MainTest(m) } diff --git a/models/migrations/v1_18/v229_test.go b/models/migrations/v1_18/v229_test.go index 5722dd3557..638983ad0b 100644 --- a/models/migrations/v1_18/v229_test.go +++ b/models/migrations/v1_18/v229_test.go @@ -7,7 +7,7 @@ import ( "testing" "code.gitea.io/gitea/models/issues" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "github.com/stretchr/testify/assert" ) @@ -16,7 +16,7 @@ func Test_UpdateOpenMilestoneCounts(t *testing.T) { type ExpectedMilestone issues.Milestone // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(issues.Milestone), new(ExpectedMilestone), new(issues.Issue)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(issues.Milestone), new(ExpectedMilestone), new(issues.Issue)) defer deferable() if x == nil || t.Failed() { return diff --git a/models/migrations/v1_18/v230_test.go b/models/migrations/v1_18/v230_test.go index 25b2f6525d..e5e28ea63f 100644 --- a/models/migrations/v1_18/v230_test.go +++ b/models/migrations/v1_18/v230_test.go @@ -6,7 +6,7 @@ package v1_18 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "github.com/stretchr/testify/assert" ) @@ -18,7 +18,7 @@ func Test_AddConfidentialClientColumnToOAuth2ApplicationTable(t *testing.T) { } // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(oauth2Application)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(oauth2Application)) defer deferable() if x == nil || t.Failed() { return diff --git a/models/migrations/v1_19/main_test.go b/models/migrations/v1_19/main_test.go index 87e807be6e..784ca0e46e 100644 --- a/models/migrations/v1_19/main_test.go +++ b/models/migrations/v1_19/main_test.go @@ -6,9 +6,9 @@ package v1_19 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" ) func TestMain(m *testing.M) { - base.MainTest(m) + migrationtest.MainTest(m) } diff --git a/models/migrations/v1_19/v233_test.go b/models/migrations/v1_19/v233_test.go index 7436ff7483..3f7900c58f 100644 --- a/models/migrations/v1_19/v233_test.go +++ b/models/migrations/v1_19/v233_test.go @@ -6,7 +6,7 @@ package v1_19 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/secret" "code.gitea.io/gitea/modules/setting" @@ -39,7 +39,7 @@ func Test_AddHeaderAuthorizationEncryptedColWebhook(t *testing.T) { } // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(Webhook), new(ExpectedWebhook), new(HookTask)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(Webhook), new(ExpectedWebhook), new(HookTask)) defer deferable() if x == nil || t.Failed() { return diff --git a/models/migrations/v1_20/main_test.go b/models/migrations/v1_20/main_test.go index 2fd63a7118..3ceb9a3c66 100644 --- a/models/migrations/v1_20/main_test.go +++ b/models/migrations/v1_20/main_test.go @@ -6,9 +6,9 @@ package v1_20 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" ) func TestMain(m *testing.M) { - base.MainTest(m) + migrationtest.MainTest(m) } diff --git a/models/migrations/v1_20/v259_test.go b/models/migrations/v1_20/v259_test.go index 0bf63719e5..3864eecb78 100644 --- a/models/migrations/v1_20/v259_test.go +++ b/models/migrations/v1_20/v259_test.go @@ -8,7 +8,7 @@ import ( "strings" "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "github.com/stretchr/testify/assert" ) @@ -66,7 +66,7 @@ func Test_ConvertScopedAccessTokens(t *testing.T) { }) } - x, deferable := base.PrepareTestEnv(t, 0, new(AccessToken)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(AccessToken)) defer deferable() if x == nil || t.Failed() { t.Skip() diff --git a/models/migrations/v1_21/main_test.go b/models/migrations/v1_21/main_test.go index 536a7ade08..daf98d40f4 100644 --- a/models/migrations/v1_21/main_test.go +++ b/models/migrations/v1_21/main_test.go @@ -6,9 +6,9 @@ package v1_21 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" ) func TestMain(m *testing.M) { - base.MainTest(m) + migrationtest.MainTest(m) } diff --git a/models/migrations/v1_22/main_test.go b/models/migrations/v1_22/main_test.go index ac8facd6aa..e02c8a5328 100644 --- a/models/migrations/v1_22/main_test.go +++ b/models/migrations/v1_22/main_test.go @@ -6,9 +6,9 @@ package v1_22 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" ) func TestMain(m *testing.M) { - base.MainTest(m) + migrationtest.MainTest(m) } diff --git a/models/migrations/v1_22/v283_test.go b/models/migrations/v1_22/v283_test.go index 743f860466..8e4c9410bd 100644 --- a/models/migrations/v1_22/v283_test.go +++ b/models/migrations/v1_22/v283_test.go @@ -6,7 +6,7 @@ package v1_22 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "github.com/stretchr/testify/assert" ) @@ -21,7 +21,7 @@ func Test_AddCombinedIndexToIssueUser(t *testing.T) { } // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(IssueUser)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(IssueUser)) defer deferable() assert.NoError(t, AddCombinedIndexToIssueUser(x)) diff --git a/models/migrations/v1_22/v286_test.go b/models/migrations/v1_22/v286_test.go index b4a50f6fcb..1bd7fac2f1 100644 --- a/models/migrations/v1_22/v286_test.go +++ b/models/migrations/v1_22/v286_test.go @@ -6,7 +6,7 @@ package v1_22 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "github.com/stretchr/testify/assert" "xorm.io/xorm" @@ -64,7 +64,7 @@ func PrepareOldRepository(t *testing.T) (*xorm.Engine, func()) { } // Prepare and load the testing database - return base.PrepareTestEnv(t, 0, + return migrationtest.PrepareTestEnv(t, 0, new(Repository), new(CommitStatus), new(RepoArchiver), diff --git a/models/migrations/v1_22/v287_test.go b/models/migrations/v1_22/v287_test.go index 2b42a33c38..21946a662a 100644 --- a/models/migrations/v1_22/v287_test.go +++ b/models/migrations/v1_22/v287_test.go @@ -7,7 +7,7 @@ import ( "strconv" "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "github.com/stretchr/testify/assert" ) @@ -20,7 +20,7 @@ func Test_UpdateBadgeColName(t *testing.T) { } // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(Badge)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(Badge)) defer deferable() if x == nil || t.Failed() { return diff --git a/models/migrations/v1_22/v293_test.go b/models/migrations/v1_22/v293_test.go index c7b643c7e0..bc3a33055c 100644 --- a/models/migrations/v1_22/v293_test.go +++ b/models/migrations/v1_22/v293_test.go @@ -6,7 +6,7 @@ package v1_22 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "code.gitea.io/gitea/models/project" "github.com/stretchr/testify/assert" @@ -14,7 +14,7 @@ import ( func Test_CheckProjectColumnsConsistency(t *testing.T) { // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(project.Project), new(project.Column)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(project.Project), new(project.Column)) defer deferable() if x == nil || t.Failed() { return diff --git a/models/migrations/v1_22/v294_test.go b/models/migrations/v1_22/v294_test.go index 1cf03d6120..a711b5ec5f 100644 --- a/models/migrations/v1_22/v294_test.go +++ b/models/migrations/v1_22/v294_test.go @@ -6,7 +6,7 @@ package v1_22 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "github.com/stretchr/testify/assert" "xorm.io/xorm/schemas" @@ -20,7 +20,7 @@ func Test_AddUniqueIndexForProjectIssue(t *testing.T) { } // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(ProjectIssue)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(ProjectIssue)) defer deferable() if x == nil || t.Failed() { return diff --git a/models/migrations/v1_23/main_test.go b/models/migrations/v1_23/main_test.go index f7b2caed83..ffccac0fd3 100644 --- a/models/migrations/v1_23/main_test.go +++ b/models/migrations/v1_23/main_test.go @@ -6,9 +6,9 @@ package v1_23 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" ) func TestMain(m *testing.M) { - base.MainTest(m) + migrationtest.MainTest(m) } diff --git a/models/migrations/v1_23/v302_test.go b/models/migrations/v1_23/v302_test.go index b008b6fc03..1832adf39a 100644 --- a/models/migrations/v1_23/v302_test.go +++ b/models/migrations/v1_23/v302_test.go @@ -6,7 +6,7 @@ package v1_23 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "code.gitea.io/gitea/modules/timeutil" "github.com/stretchr/testify/assert" @@ -44,7 +44,7 @@ func Test_AddIndexToActionTaskStoppedLogExpired(t *testing.T) { } // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(ActionTask)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(ActionTask)) defer deferable() assert.NoError(t, AddIndexToActionTaskStoppedLogExpired(x)) diff --git a/models/migrations/v1_23/v304_test.go b/models/migrations/v1_23/v304_test.go index c3dfa5e7e7..9af84cd257 100644 --- a/models/migrations/v1_23/v304_test.go +++ b/models/migrations/v1_23/v304_test.go @@ -6,7 +6,7 @@ package v1_23 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "code.gitea.io/gitea/modules/timeutil" "github.com/stretchr/testify/assert" @@ -33,7 +33,7 @@ func Test_AddIndexForReleaseSha1(t *testing.T) { } // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(Release)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(Release)) defer deferable() assert.NoError(t, AddIndexForReleaseSha1(x)) diff --git a/models/migrations/v1_25/main_test.go b/models/migrations/v1_25/main_test.go index d2c4a4105d..33c981edb9 100644 --- a/models/migrations/v1_25/main_test.go +++ b/models/migrations/v1_25/main_test.go @@ -6,9 +6,9 @@ package v1_25 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" ) func TestMain(m *testing.M) { - base.MainTest(m) + migrationtest.MainTest(m) } diff --git a/models/migrations/v1_25/v321_test.go b/models/migrations/v1_25/v321_test.go index 3ef2c68aa3..0749a20e20 100644 --- a/models/migrations/v1_25/v321_test.go +++ b/models/migrations/v1_25/v321_test.go @@ -6,7 +6,7 @@ package v1_25 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" @@ -44,12 +44,12 @@ func Test_UseLongTextInSomeColumnsAndFixBugs(t *testing.T) { } // Prepare and load the testing database - x, deferrable := base.PrepareTestEnv(t, 0, new(ReviewState), new(PackageProperty), new(Notice)) + x, deferrable := migrationtest.PrepareTestEnv(t, 0, new(ReviewState), new(PackageProperty), new(Notice)) defer deferrable() require.NoError(t, UseLongTextInSomeColumnsAndFixBugs(x)) - tables := base.LoadTableSchemasMap(t, x) + tables := migrationtest.LoadTableSchemasMap(t, x) table := tables["review_state"] column := table.GetColumn("updated_files") assert.Equal(t, "LONGTEXT", column.SQLType.Name) diff --git a/models/migrations/v1_25/v322_test.go b/models/migrations/v1_25/v322_test.go index 78d890704c..1964614035 100644 --- a/models/migrations/v1_25/v322_test.go +++ b/models/migrations/v1_25/v322_test.go @@ -6,7 +6,7 @@ package v1_25 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "code.gitea.io/gitea/modules/setting" "github.com/stretchr/testify/assert" @@ -23,11 +23,11 @@ func Test_ExtendCommentTreePathLength(t *testing.T) { TreePath string `xorm:"VARCHAR(255)"` } - x, deferrable := base.PrepareTestEnv(t, 0, new(Comment)) + x, deferrable := migrationtest.PrepareTestEnv(t, 0, new(Comment)) defer deferrable() require.NoError(t, ExtendCommentTreePathLength(x)) - table := base.LoadTableSchemasMap(t, x)["comment"] + table := migrationtest.LoadTableSchemasMap(t, x)["comment"] column := table.GetColumn("tree_path") assert.Contains(t, []string{"NVARCHAR", "VARCHAR"}, column.SQLType.Name) assert.EqualValues(t, 4000, column.Length) diff --git a/models/migrations/v1_26/main_test.go b/models/migrations/v1_26/main_test.go index 5aa12d553c..0b271b9bbc 100644 --- a/models/migrations/v1_26/main_test.go +++ b/models/migrations/v1_26/main_test.go @@ -6,9 +6,9 @@ package v1_26 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" ) func TestMain(m *testing.M) { - base.MainTest(m) + migrationtest.MainTest(m) } diff --git a/models/migrations/v1_26/v325_test.go b/models/migrations/v1_26/v325_test.go index d4a66fee81..3fd658e01b 100644 --- a/models/migrations/v1_26/v325_test.go +++ b/models/migrations/v1_26/v325_test.go @@ -6,7 +6,7 @@ package v1_26 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "code.gitea.io/gitea/modules/timeutil" "github.com/stretchr/testify/require" @@ -38,7 +38,7 @@ func Test_FixMissedRepoIDWhenMigrateAttachments(t *testing.T) { } // Prepare and load the testing database - x, deferrable := base.PrepareTestEnv(t, 0, new(Attachment), new(Issue), new(Release)) + x, deferrable := migrationtest.PrepareTestEnv(t, 0, new(Attachment), new(Issue), new(Release)) defer deferrable() require.NoError(t, FixMissedRepoIDWhenMigrateAttachments(x)) diff --git a/models/migrations/v1_26/v326_test.go b/models/migrations/v1_26/v326_test.go index b92eed35f6..a0225eb774 100644 --- a/models/migrations/v1_26/v326_test.go +++ b/models/migrations/v1_26/v326_test.go @@ -6,7 +6,7 @@ package v1_26 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/test" @@ -57,7 +57,7 @@ func Test_FixCommitStatusTargetURLToUseRunAndJobID(t *testing.T) { TargetURL string } - x, deferable := base.PrepareTestEnv(t, 0, + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(Repository), new(ActionRun), new(ActionRunJob), diff --git a/models/migrations/v1_26/v327_test.go b/models/migrations/v1_26/v327_test.go index 971707be4f..98e948cf05 100644 --- a/models/migrations/v1_26/v327_test.go +++ b/models/migrations/v1_26/v327_test.go @@ -6,7 +6,7 @@ package v1_26 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "github.com/stretchr/testify/require" ) @@ -17,7 +17,7 @@ func Test_AddDisabledToActionRunner(t *testing.T) { Name string } - x, deferable := base.PrepareTestEnv(t, 0, new(ActionRunner)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(ActionRunner)) defer deferable() _, err := x.Insert(&ActionRunner{Name: "runner"}) diff --git a/models/migrations/v1_26/v329_test.go b/models/migrations/v1_26/v329_test.go index cab8e79906..e4bebfb71d 100644 --- a/models/migrations/v1_26/v329_test.go +++ b/models/migrations/v1_26/v329_test.go @@ -6,7 +6,7 @@ package v1_26 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "github.com/stretchr/testify/assert" ) @@ -22,7 +22,7 @@ func (UserBadgeBefore) TableName() string { } func Test_AddUniqueIndexForUserBadge(t *testing.T) { - x, deferable := base.PrepareTestEnv(t, 0, new(UserBadgeBefore)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(UserBadgeBefore)) defer deferable() if x == nil || t.Failed() { return diff --git a/models/migrations/v1_27/main_test.go b/models/migrations/v1_27/main_test.go index e269e3df9a..0c6a6a2440 100644 --- a/models/migrations/v1_27/main_test.go +++ b/models/migrations/v1_27/main_test.go @@ -6,9 +6,9 @@ package v1_27 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" ) func TestMain(m *testing.M) { - base.MainTest(m) + migrationtest.MainTest(m) } diff --git a/models/migrations/v1_27/v331_test.go b/models/migrations/v1_27/v331_test.go index 45f467cf9b..2302fee024 100644 --- a/models/migrations/v1_27/v331_test.go +++ b/models/migrations/v1_27/v331_test.go @@ -8,7 +8,7 @@ import ( "slices" "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -49,7 +49,7 @@ func (actionArtifactBeforeV331) TableName() string { } func Test_AddActionRunAttemptModel(t *testing.T) { - x, deferable := base.PrepareTestEnv(t, 0, + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(actionRunBeforeV331), new(actionRunJobBeforeV331), new(actionArtifactBeforeV331), @@ -69,7 +69,7 @@ func Test_AddActionRunAttemptModel(t *testing.T) { require.NoError(t, AddActionRunAttemptModel(x)) - tableMap := base.LoadTableSchemasMap(t, x) + tableMap := migrationtest.LoadTableSchemasMap(t, x) attemptTable := tableMap["action_run_attempt"] require.NotNil(t, attemptTable) diff --git a/models/unittest/testdb.go b/models/unittest/testdb.go index bd832348e7..116fdab496 100644 --- a/models/unittest/testdb.go +++ b/models/unittest/testdb.go @@ -5,6 +5,8 @@ package unittest import ( "context" + "database/sql" + "errors" "fmt" "os" "path/filepath" @@ -102,6 +104,101 @@ func mainTest(m *testing.M, testOptsArg ...*TestOptions) int { return exitStatus } +func ResetTestDatabase() (cleanup func(), err error) { + defer func() { + if cleanup == nil { + cleanup = func() {} + } + }() + + connOpts := db.GlobalConnOptions() + driverDefault, connStrDefault, err := db.ConnStrDefaultDatabase(connOpts) + if err != nil { + return nil, err + } + driverDatabase, connStrDatabase, err := db.ConnStr(connOpts) + if err != nil { + return nil, err + } + + if connOpts.Type.IsSQLite3() { + if !strings.HasSuffix(connOpts.SQLitePath, "-test.db") { + return nil, errors.New(`testing database file for sqlite3 must end in "-test.db"`) + } + _ = os.Remove(connOpts.SQLitePath) + err = os.MkdirAll(filepath.Dir(connOpts.SQLitePath), os.ModePerm) + if err != nil { + return nil, err + } + cleanup = func() { + _ = os.Remove(connOpts.SQLitePath) + _ = os.Remove(filepath.Dir(connOpts.SQLitePath)) + } + return cleanup, nil + } + + if !strings.Contains(connOpts.Database, "test") { + return nil, fmt.Errorf(`testing database name for %s must contain "test"`, connOpts.Database) + } + + quotedDbName := connOpts.Database + if connOpts.Type.IsMSSQL() { + quotedDbName = `[` + connOpts.Database + `]` + } + + sqlExec := func(sqlDB *sql.DB, sql string) error { + _, err := sqlDB.Exec(sql) + if err != nil { + return fmt.Errorf("failed to execute SQL %q: %w", sql, err) + } + return nil + } + + createDatabase := func() error { + sqlDB, err := sql.Open(driverDefault, connStrDefault) + if err != nil { + return err + } + defer sqlDB.Close() + if err = sqlExec(sqlDB, "DROP DATABASE IF EXISTS "+quotedDbName); err != nil { + return err + } + return sqlExec(sqlDB, "CREATE DATABASE "+quotedDbName) + } + if err = createDatabase(); err != nil { + return nil, err + } + + cleanup = func() { + sqlDB, err := sql.Open(driverDefault, connStrDefault) + if err != nil { + return + } + defer sqlDB.Close() + _, _ = sqlDB.Exec("DROP DATABASE IF EXISTS " + quotedDbName) + } + + createDatabaseSchema := func() error { + if !connOpts.Type.IsPostgreSQL() { + return nil + } + if connOpts.Schema == "" { + return nil + } + sqlDB, err := sql.Open(driverDatabase, connStrDatabase) + if err != nil { + return err + } + defer sqlDB.Close() + if err = sqlExec(sqlDB, "DROP SCHEMA IF EXISTS "+connOpts.Schema); err != nil { + return err + } + return sqlExec(sqlDB, "CREATE SCHEMA "+connOpts.Schema) + } + + return cleanup, createDatabaseSchema() +} + // FixturesOptions fixtures needs to be loaded options type FixturesOptions struct { Dir string @@ -110,11 +207,12 @@ type FixturesOptions struct { // CreateTestEngine creates a memory database and loads the fixture data from fixturesDir func CreateTestEngine(opts FixturesOptions) error { - x, err := xorm.NewEngine("sqlite3", "file::memory:?cache=shared&_txlock=immediate") + driver, connStr, err := db.ConnStr(db.ConnOptions{Type: "sqlite3", SQLitePath: ":memory:"}) + if err != nil { + return err + } + x, err := xorm.NewEngine(driver, connStr) if err != nil { - if strings.Contains(err.Error(), "unknown driver") { - return fmt.Errorf("sqlite3 requires: -tags sqlite,sqlite_unlock_notify\n%w", err) - } return err } x.SetMapper(names.GonicMapper{}) diff --git a/modules/setting/database.go b/modules/setting/database.go index 1a4bf64805..2b069a6292 100644 --- a/modules/setting/database.go +++ b/modules/setting/database.go @@ -4,13 +4,7 @@ package setting import ( - "errors" - "fmt" - "net" - "net/url" - "os" "path/filepath" - "strings" "time" ) @@ -20,24 +14,22 @@ var ( // DatabaseTypeNames contains the friendly names for all database types DatabaseTypeNames = map[string]string{"mysql": "MySQL", "postgres": "PostgreSQL", "mssql": "MSSQL", "sqlite3": "SQLite3"} - // EnableSQLite3 use SQLite3, set by build flag - EnableSQLite3 bool - // Database holds the database settings Database = struct { - Type DatabaseType - Host string - Name string - User string - Passwd string - Schema string - SSLMode string - Path string + Type DatabaseType + Host string + Name string + User string + Passwd string + Schema string + SSLMode string + Path string + + SQLiteBusyTimeout int + SQLiteJournalMode string + LogSQL bool - MysqlCharset string CharsetCollation string - Timeout int // seconds - SQLiteJournalMode string DBConnectRetries int DBConnectBackoff time.Duration MaxIdleConns int @@ -47,7 +39,7 @@ var ( AutoMigration bool SlowQueryThreshold time.Duration }{ - Timeout: 500, + SQLiteBusyTimeout: 500, IterateBufferSize: 50, } ) @@ -64,15 +56,14 @@ func loadDBSetting(rootCfg ConfigProvider) { Database.Host = sec.Key("HOST").String() Database.Name = sec.Key("NAME").String() Database.User = sec.Key("USER").String() - if len(Database.Passwd) == 0 { - Database.Passwd = sec.Key("PASSWD").String() - } + Database.Passwd = sec.Key("PASSWD").String() + Database.Schema = sec.Key("SCHEMA").String() Database.SSLMode = sec.Key("SSL_MODE").MustString("disable") Database.CharsetCollation = sec.Key("CHARSET_COLLATION").String() Database.Path = sec.Key("PATH").MustString(filepath.Join(AppDataPath, "gitea.db")) - Database.Timeout = sec.Key("SQLITE_TIMEOUT").MustInt(500) + Database.SQLiteBusyTimeout = sec.Key("SQLITE_TIMEOUT").MustInt(500) Database.SQLiteJournalMode = sec.Key("SQLITE_JOURNAL_MODE").MustString("") Database.MaxIdleConns = sec.Key("MAX_IDLE_CONNS").MustInt(2) @@ -91,123 +82,9 @@ func loadDBSetting(rootCfg ConfigProvider) { Database.SlowQueryThreshold = sec.Key("SLOW_QUERY_THRESHOLD").MustDuration(5 * time.Second) } -// DBConnStr returns database connection string -func DBConnStr() (string, error) { - var connStr string - paramSep := "?" - if strings.Contains(Database.Name, paramSep) { - paramSep = "&" - } - switch Database.Type { - case "mysql": - connType := "tcp" - if len(Database.Host) > 0 && Database.Host[0] == '/' { // looks like a unix socket - connType = "unix" - } - tls := Database.SSLMode - if tls == "disable" { // allow (Postgres-inspired) default value to work in MySQL - tls = "false" - } - connStr = fmt.Sprintf("%s:%s@%s(%s)/%s%sparseTime=true&tls=%s", - Database.User, Database.Passwd, connType, Database.Host, Database.Name, paramSep, tls) - case "postgres": - connStr = getPostgreSQLConnectionString(Database.Host, Database.User, Database.Passwd, Database.Name, Database.SSLMode) - case "mssql": - host, port := ParseMSSQLHostPort(Database.Host) - connStr = fmt.Sprintf("server=%s; port=%s; database=%s; user id=%s; password=%s;", host, port, Database.Name, Database.User, Database.Passwd) - case "sqlite3": - if !EnableSQLite3 { - return "", errors.New("this Gitea binary was not built with SQLite3 support") - } - if err := os.MkdirAll(filepath.Dir(Database.Path), os.ModePerm); err != nil { - return "", fmt.Errorf("Failed to create directories: %w", err) - } - journalMode := "" - if Database.SQLiteJournalMode != "" { - journalMode = "&_journal_mode=" + Database.SQLiteJournalMode - } - connStr = fmt.Sprintf("file:%s?cache=shared&mode=rwc&_busy_timeout=%d&_txlock=immediate%s", - Database.Path, Database.Timeout, journalMode) - default: - return "", fmt.Errorf("unknown database type: %s", Database.Type) - } - - return connStr, nil -} - -// parsePostgreSQLHostPort parses given input in various forms defined in -// https://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-CONNSTRING -// and returns proper host and port number. -func parsePostgreSQLHostPort(info string) (host, port string) { - if h, p, err := net.SplitHostPort(info); err == nil { - host, port = h, p - } else { - // treat the "info" as "host", if it's an IPv6 address, remove the wrapper - host = info - if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") { - host = host[1 : len(host)-1] - } - } - - // set fallback values - if host == "" { - host = "127.0.0.1" - } - if port == "" { - port = "5432" - } - return host, port -} - -func getPostgreSQLConnectionString(dbHost, dbUser, dbPasswd, dbName, dbsslMode string) (connStr string) { - dbName, dbParam, _ := strings.Cut(dbName, "?") - host, port := parsePostgreSQLHostPort(dbHost) - connURL := url.URL{ - Scheme: "postgres", - User: url.UserPassword(dbUser, dbPasswd), - Host: net.JoinHostPort(host, port), - Path: dbName, - OmitHost: false, - RawQuery: dbParam, - } - query := connURL.Query() - if strings.HasPrefix(host, "/") { // looks like a unix socket - query.Add("host", host) - connURL.Host = ":" + port - } - query.Set("sslmode", dbsslMode) - connURL.RawQuery = query.Encode() - return connURL.String() -} - -// ParseMSSQLHostPort splits the host into host and port -func ParseMSSQLHostPort(info string) (string, string) { - // the default port "0" might be related to MSSQL's dynamic port, maybe it should be double-confirmed in the future - host, port := "127.0.0.1", "0" - if strings.Contains(info, ":") { - host = strings.Split(info, ":")[0] - port = strings.Split(info, ":")[1] - } else if strings.Contains(info, ",") { - host = strings.Split(info, ",")[0] - port = strings.TrimSpace(strings.Split(info, ",")[1]) - } else if len(info) > 0 { - host = info - } - if host == "" { - host = "127.0.0.1" - } - if port == "" { - port = "0" - } - return host, port -} - +// DatabaseType FIXME: it is also used directly with "schemas.DBType", so the names must be consistent type DatabaseType string -func (t DatabaseType) String() string { - return string(t) -} - func (t DatabaseType) IsSQLite3() bool { return t == "sqlite3" } diff --git a/modules/setting/database_sqlite.go b/modules/setting/database_sqlite.go deleted file mode 100644 index c1037cfb27..0000000000 --- a/modules/setting/database_sqlite.go +++ /dev/null @@ -1,15 +0,0 @@ -//go:build sqlite - -// Copyright 2014 The Gogs Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package setting - -import ( - _ "github.com/mattn/go-sqlite3" -) - -func init() { - EnableSQLite3 = true - SupportedDatabaseTypes = append(SupportedDatabaseTypes, "sqlite3") -} diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index 9c6903edc6..80692acdaf 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -3315,7 +3315,6 @@ "admin.config.cache_config": "Cache Configuration", "admin.config.cache_adapter": "Cache Adapter", "admin.config.cache_interval": "Cache Interval", - "admin.config.cache_conn": "Cache Connection", "admin.config.cache_item_ttl": "Cache Item TTL", "admin.config.cache_test": "Test Cache", "admin.config.cache_test_failed": "Failed to probe the cache: %v.", @@ -3330,7 +3329,6 @@ "admin.config.instance_web_banner.message_placeholder": "Banner message (supports markdown)", "admin.config.session_config": "Session Configuration", "admin.config.session_provider": "Session Provider", - "admin.config.provider_config": "Provider Config", "admin.config.cookie_name": "Cookie Name", "admin.config.gc_interval_time": "GC Interval Time", "admin.config.session_life_time": "Session Life Time", diff --git a/routers/init.go b/routers/init.go index 92eab5eaf2..e04b711c4d 100644 --- a/routers/init.go +++ b/routers/init.go @@ -134,12 +134,6 @@ func InitWebInstalled(ctx context.Context) { external.RegisterRenderers() markup.Init(markup_service.FormalRenderHelperFuncs()) - if setting.EnableSQLite3 { - log.Info("SQLite3 support is enabled") - } else if setting.Database.Type.IsSQLite3() { - log.Fatal("SQLite3 support is disabled, but it is used for database setting. Please get or build a Gitea release with SQLite3 support.") - } - mustInitCtx(ctx, common.InitDBEngine) log.Info("ORM engine initialization successful!") mustInit(system.Init) diff --git a/routers/install/install.go b/routers/install/install.go index a0f32fb939..718ede6564 100644 --- a/routers/install/install.go +++ b/routers/install/install.go @@ -76,7 +76,7 @@ func Install(ctx *context.Context) { form.DbSchema = setting.Database.Schema form.SSLMode = setting.Database.SSLMode - curDBType := setting.Database.Type.String() + curDBType := string(setting.Database.Type) if !slices.Contains(setting.SupportedDatabaseTypes, curDBType) { curDBType = "mysql" } @@ -328,7 +328,7 @@ func SubmitInstall(ctx *context.Context) { cfg.Section("").Key("WORK_PATH").SetValue(setting.AppWorkPath) cfg.Section("").Key("RUN_MODE").SetValue("prod") - cfg.Section("database").Key("DB_TYPE").SetValue(setting.Database.Type.String()) + cfg.Section("database").Key("DB_TYPE").SetValue(string(setting.Database.Type)) cfg.Section("database").Key("HOST").SetValue(setting.Database.Host) cfg.Section("database").Key("NAME").SetValue(setting.Database.Name) cfg.Section("database").Key("USER").SetValue(setting.Database.User) diff --git a/routers/web/admin/admin_test.go b/routers/web/admin/admin_test.go index ecdd462f9e..929faa1968 100644 --- a/routers/web/admin/admin_test.go +++ b/routers/web/admin/admin_test.go @@ -16,64 +16,6 @@ import ( "github.com/stretchr/testify/require" ) -func TestShadowPassword(t *testing.T) { - kases := []struct { - Provider string - CfgItem string - Result string - }{ - { - Provider: "redis", - CfgItem: "network=tcp,addr=:6379,password=gitea,db=0,pool_size=100,idle_timeout=180", - Result: "network=tcp,addr=:6379,password=******,db=0,pool_size=100,idle_timeout=180", - }, - { - Provider: "mysql", - CfgItem: "root:@tcp(localhost:3306)/gitea?charset=utf8", - Result: "root:******@tcp(localhost:3306)/gitea?charset=utf8", - }, - { - Provider: "mysql", - CfgItem: "/gitea?charset=utf8", - Result: "/gitea?charset=utf8", - }, - { - Provider: "mysql", - CfgItem: "user:mypassword@/dbname", - Result: "user:******@/dbname", - }, - { - Provider: "postgres", - CfgItem: "user=pqgotest dbname=pqgotest sslmode=verify-full", - Result: "user=pqgotest dbname=pqgotest sslmode=verify-full", - }, - { - Provider: "postgres", - CfgItem: "user=pqgotest password= dbname=pqgotest sslmode=verify-full", - Result: "user=pqgotest password=****** dbname=pqgotest sslmode=verify-full", - }, - { - Provider: "postgres", - CfgItem: "postgres://user:pass@hostname/dbname", - Result: "postgres://user:******@hostname/dbname", - }, - { - Provider: "couchbase", - CfgItem: "http://dev-couchbase.example.com:8091/", - Result: "http://dev-couchbase.example.com:8091/", - }, - { - Provider: "couchbase", - CfgItem: "http://user:the_password@dev-couchbase.example.com:8091/", - Result: "http://user:******@dev-couchbase.example.com:8091/", - }, - } - - for _, k := range kases { - assert.Equal(t, k.Result, shadowPassword(k.Provider, k.CfgItem)) - } -} - func TestSelfCheckPost(t *testing.T) { defer test.MockVariableValue(&setting.PublicURLDetection)() defer test.MockVariableValue(&setting.AppURL, "http://config/sub/")() diff --git a/routers/web/admin/config.go b/routers/web/admin/config.go index bf48e554df..03a15b6713 100644 --- a/routers/web/admin/config.go +++ b/routers/web/admin/config.go @@ -7,8 +7,6 @@ package admin import ( "errors" "net/http" - "net/url" - "strings" system_model "code.gitea.io/gitea/models/system" "code.gitea.io/gitea/modules/cache" @@ -59,63 +57,6 @@ func TestCache(ctx *context.Context) { ctx.Redirect(setting.AppSubURL + "/-/admin/config") } -func shadowPasswordKV(cfgItem, splitter string) string { - fields := strings.Split(cfgItem, splitter) - for i := range fields { - if strings.HasPrefix(fields[i], "password=") { - fields[i] = "password=******" - break - } - } - return strings.Join(fields, splitter) -} - -func shadowURL(provider, cfgItem string) string { - u, err := url.Parse(cfgItem) - if err != nil { - log.Error("Shadowing Password for %v failed: %v", provider, err) - return cfgItem - } - if u.User != nil { - atIdx := strings.Index(cfgItem, "@") - if atIdx > 0 { - colonIdx := strings.LastIndex(cfgItem[:atIdx], ":") - if colonIdx > 0 { - return cfgItem[:colonIdx+1] + "******" + cfgItem[atIdx:] - } - } - } - return cfgItem -} - -func shadowPassword(provider, cfgItem string) string { - switch provider { - case "redis": - return shadowPasswordKV(cfgItem, ",") - case "mysql": - // root:@tcp(localhost:3306)/macaron?charset=utf8 - atIdx := strings.Index(cfgItem, "@") - if atIdx > 0 { - colonIdx := strings.Index(cfgItem[:atIdx], ":") - if colonIdx > 0 { - return cfgItem[:colonIdx+1] + "******" + cfgItem[atIdx:] - } - } - return cfgItem - case "postgres": - // user=jiahuachen dbname=macaron port=5432 sslmode=disable - if !strings.HasPrefix(cfgItem, "postgres://") { - return shadowPasswordKV(cfgItem, " ") - } - fallthrough - case "couchbase": - return shadowURL(provider, cfgItem) - // postgres://pqgotest:password@localhost/pqgotest?sslmode=verify-full - // Notice: use shadowURL - } - return cfgItem -} - // Config show admin config page func Config(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("admin.config_summary") @@ -150,8 +91,6 @@ func Config(ctx *context.Context) { ctx.Data["CacheAdapter"] = setting.CacheService.Adapter ctx.Data["CacheInterval"] = setting.CacheService.Interval - - ctx.Data["CacheConn"] = shadowPassword(setting.CacheService.Adapter, setting.CacheService.Conn) ctx.Data["CacheItemTTL"] = setting.CacheService.TTL sessionCfg := setting.SessionConfig @@ -169,7 +108,7 @@ func Config(ctx *context.Context) { sessionCfg.Secure = realSession.Secure sessionCfg.Domain = realSession.Domain } - sessionCfg.ProviderConfig = shadowPassword(sessionCfg.Provider, sessionCfg.ProviderConfig) + sessionCfg.ProviderConfig = "" ctx.Data["SessionConfig"] = sessionCfg ctx.Data["Git"] = setting.Git diff --git a/routers/web/healthcheck/check.go b/routers/web/healthcheck/check.go index de9b2c8ec1..116aab886b 100644 --- a/routers/web/healthcheck/check.go +++ b/routers/web/healthcheck/check.go @@ -111,16 +111,10 @@ func checkDatabase(ctx context.Context, checks checks) status { } if setting.Database.Type.IsSQLite3() && st.Status == pass { - if !setting.EnableSQLite3 { + if _, err := os.Stat(setting.Database.Path); err != nil { st.Status = fail st.Time = getCheckTime() - log.Error("SQLite3 health check failed with error: %v", "this Gitea binary is built without SQLite3 enabled") - } else { - if _, err := os.Stat(setting.Database.Path); err != nil { - st.Status = fail - st.Time = getCheckTime() - log.Error("SQLite3 file exists check failed with error: %v", err) - } + log.Error("SQLite3 file exists check failed with error: %v", err) } } diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl index b68f2c1a7a..c381c5bf1d 100644 --- a/templates/admin/config.tmpl +++ b/templates/admin/config.tmpl @@ -244,8 +244,6 @@
{{.CacheInterval}} {{ctx.Locale.Tr "tool.raw_seconds"}}
{{end}} {{if .CacheConn}} -
{{ctx.Locale.Tr "admin.config.cache_conn"}}
-
{{.CacheConn}}
{{ctx.Locale.Tr "admin.config.cache_item_ttl"}}
{{.CacheItemTTL}}
{{end}} @@ -266,8 +264,6 @@
{{ctx.Locale.Tr "admin.config.session_provider"}}
{{.SessionConfig.Provider}}
-
{{ctx.Locale.Tr "admin.config.provider_config"}}
-
{{if .SessionConfig.ProviderConfig}}{{.SessionConfig.ProviderConfig}}{{else}}-{{end}}
{{ctx.Locale.Tr "admin.config.cookie_name"}}
{{.SessionConfig.CookieName}}
{{ctx.Locale.Tr "admin.config.gc_interval_time"}}
diff --git a/tests/integration/migration-test/gitea-v1.6.4.mssql.sql.gz b/tests/integration/migration-test/gitea-v1.6.4.mssql.sql.gz index 1b676feda1cbced0754c4ce0751e159e4a078e6f..85ba253cf88183a77afb8ee4ad58b1892789cb58 100644 GIT binary patch literal 13260 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 F0RSw%47&gT literal 12969 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 MPkx8SE@ol?0F9u9%m4rY diff --git a/tests/integration/migration-test/gitea-v1.7.0.mssql.sql.gz b/tests/integration/migration-test/gitea-v1.7.0.mssql.sql.gz index bd869cfa5836cddd10010afefa33cc000fefb44d..4f36c7c0cf162f8d257f9102dccc333eadb395f9 100644 GIT binary patch literal 13369 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 literal 13068 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 z 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.+)" + regexp.QuoteMeta("."+setting.Database.Type.String()+".sql.gz")) + versionRE, err := regexp.Compile("gitea-v(?P.+)" + regexp.QuoteMeta("."+string(setting.Database.Type)+".sql.gz")) if err != nil { return nil, err } @@ -64,7 +62,7 @@ func availableVersions() ([]string, error) { if err != nil { return nil, err } - versions := []string{} + var versions []string for _, filename := range filenames { if versionRE.MatchString(filename) { substrings := versionRE.FindStringSubmatch(filename) @@ -76,11 +74,8 @@ func availableVersions() ([]string, error) { } func readSQLFromFile(version string) (string, error) { - filename := filepath.Join(setting.GetGiteaTestSourceRoot(), "tests/integration/migration-test", fmt.Sprintf("gitea-v%s.%s.sql.gz", version, setting.Database.Type)) - - if _, err := os.Stat(filename); os.IsNotExist(err) { - return "", nil - } + filename := fmt.Sprintf("tests/integration/migration-test/gitea-v%s.%s.sql.gz", version, setting.Database.Type) + filename = filepath.Join(setting.GetGiteaTestSourceRoot(), filename) file, err := os.Open(filename) if err != nil { @@ -106,134 +101,51 @@ func restoreOldDB(t *testing.T, version string) { require.NoError(t, err) require.NotEmpty(t, data, "No data found for %s version: %s", setting.Database.Type, version) - switch { - case setting.Database.Type.IsSQLite3(): - util.Remove(setting.Database.Path) - err := os.MkdirAll(path.Dir(setting.Database.Path), os.ModePerm) - assert.NoError(t, err) + cleanup, err := unittest.ResetTestDatabase() + require.NoError(t, err) + _ = cleanup // no clean up yet (not needed at the moment) - db, err := sql.Open("sqlite3", fmt.Sprintf("file:%s?cache=shared&mode=rwc&_busy_timeout=%d&_txlock=immediate", setting.Database.Path, setting.Database.Timeout)) - assert.NoError(t, err) - defer db.Close() + connOpts := db.GlobalConnOptions() - _, err = db.Exec(data) - assert.NoError(t, err) - db.Close() - - case setting.Database.Type.IsMySQL(): - db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s)/", - setting.Database.User, setting.Database.Passwd, setting.Database.Host)) - assert.NoError(t, err) - defer db.Close() - - _, err = db.Exec("DROP DATABASE IF EXISTS " + setting.Database.Name) - assert.NoError(t, err) - - _, err = db.Exec("CREATE DATABASE IF NOT EXISTS " + setting.Database.Name) - assert.NoError(t, err) - db.Close() - - db, err = sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s)/%s?multiStatements=true", - setting.Database.User, setting.Database.Passwd, setting.Database.Host, setting.Database.Name)) - assert.NoError(t, err) - defer db.Close() - - _, err = db.Exec(data) - assert.NoError(t, err) - db.Close() - - case setting.Database.Type.IsPostgreSQL(): - var db *sql.DB - var err error - if setting.Database.Host[0] == '/' { - db, err = sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@/?sslmode=%s&host=%s", - setting.Database.User, setting.Database.Passwd, setting.Database.SSLMode, setting.Database.Host)) - assert.NoError(t, err) - } else { - db, err = sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@%s/?sslmode=%s", - setting.Database.User, setting.Database.Passwd, setting.Database.Host, setting.Database.SSLMode)) - assert.NoError(t, err) + if !connOpts.Type.IsMSSQL() { + if connOpts.Type.IsMySQL() { + connOpts.Database += "?multiStatements=true" } - defer db.Close() + driver, connStr, err := db.ConnStr(connOpts) + require.NoError(t, err) - _, err = db.Exec("DROP DATABASE IF EXISTS " + setting.Database.Name) - assert.NoError(t, err) + sqlDB, err := sql.Open(driver, connStr) + require.NoError(t, err) + defer sqlDB.Close() - _, err = db.Exec("CREATE DATABASE " + setting.Database.Name) - assert.NoError(t, err) - db.Close() - - // Check if we need to setup a specific schema - if len(setting.Database.Schema) != 0 { - if setting.Database.Host[0] == '/' { - db, err = sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@/%s?sslmode=%s&host=%s", - setting.Database.User, setting.Database.Passwd, setting.Database.Name, setting.Database.SSLMode, setting.Database.Host)) - } else { - db, err = sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=%s", - setting.Database.User, setting.Database.Passwd, setting.Database.Host, setting.Database.Name, setting.Database.SSLMode)) - } - require.NoError(t, err) - defer db.Close() - - schrows, err := db.Query(fmt.Sprintf("SELECT 1 FROM information_schema.schemata WHERE schema_name = '%s'", setting.Database.Schema)) - require.NoError(t, err) - require.NotEmpty(t, schrows) - - if !schrows.Next() { - // Create and setup a DB schema - _, err = db.Exec("CREATE SCHEMA " + setting.Database.Schema) - assert.NoError(t, err) - } - schrows.Close() - - // Make the user's default search path the created schema; this will affect new connections - _, err = db.Exec(fmt.Sprintf(`ALTER USER "%s" SET search_path = %s`, setting.Database.User, setting.Database.Schema)) - assert.NoError(t, err) - - db.Close() - } - - if setting.Database.Host[0] == '/' { - db, err = sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@/%s?sslmode=%s&host=%s", - setting.Database.User, setting.Database.Passwd, setting.Database.Name, setting.Database.SSLMode, setting.Database.Host)) - } else { - db, err = sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=%s", - setting.Database.User, setting.Database.Passwd, setting.Database.Host, setting.Database.Name, setting.Database.SSLMode)) - } - assert.NoError(t, err) - defer db.Close() - - _, err = db.Exec(data) - assert.NoError(t, err) - db.Close() - - case setting.Database.Type.IsMSSQL(): - host, port := setting.ParseMSSQLHostPort(setting.Database.Host) - db, err := sql.Open("mssql", fmt.Sprintf("server=%s; port=%s; database=%s; user id=%s; password=%s;", - host, port, "master", setting.Database.User, setting.Database.Passwd)) - assert.NoError(t, err) - defer db.Close() - - _, err = db.Exec("DROP DATABASE IF EXISTS [gitea]") - assert.NoError(t, err) - - statements := strings.Split(data, "\nGO\n") - for _, statement := range statements { - if len(statement) > 5 && statement[:5] == "USE [" { - dbname := statement[5 : len(statement)-1] - db.Close() - db, err = sql.Open("mssql", fmt.Sprintf("server=%s; port=%s; database=%s; user id=%s; password=%s;", - host, port, dbname, setting.Database.User, setting.Database.Passwd)) - assert.NoError(t, err) - defer db.Close() - } - _, err = db.Exec(statement) - assert.NoError(t, err, "Failure whilst running: %s\nError: %v", statement, err) - } - db.Close() - default: - assert.Failf(t, "unsupported database type", "setting.Database.Type=%v", setting.Database.Type) + _, err = sqlDB.Exec(data) + require.NoError(t, err) + return } + + // MSSQL is special. the test fixture will create the [testgitea] database again, so drop it ahead if it exists + driver, connStr, err := db.ConnStrDefaultDatabase(connOpts) + require.NoError(t, err) + sqlDB, err := sql.Open(driver, connStr) + require.NoError(t, err) + + _, err = sqlDB.Exec("DROP DATABASE IF EXISTS [testgitea]") + require.NoError(t, err, "drop existing database testgitea") + + for statement := range strings.SplitSeq(data, "\nGO\n") { + if useStmtAfter, ok := strings.CutPrefix(statement, "USE ["); ok { + _ = sqlDB.Close() + dbname := strings.TrimSuffix(useStmtAfter, "]") // extract the database name from "USE [dbname]" + connOpts.Database = dbname + driver, connStr, err := db.ConnStr(connOpts) + require.NoError(t, err) + sqlDB, err = sql.Open(driver, connStr) + require.NoError(t, err) + } + _, err = sqlDB.Exec(statement) + require.NoError(t, err, "SQL Exec failed when running: %s\nError: %v", statement, err) + } + _ = sqlDB.Close() } func wrappedMigrate(ctx context.Context, x *xorm.Engine) error { diff --git a/tests/sqlite.ini.tmpl b/tests/sqlite.ini.tmpl index 03393c620b..95a1df283f 100644 --- a/tests/sqlite.ini.tmpl +++ b/tests/sqlite.ini.tmpl @@ -4,7 +4,7 @@ RUN_MODE = prod [database] DB_TYPE = sqlite3 -PATH = gitea.db +PATH = gitea-test.db [indexer] REPO_INDEXER_ENABLED = true diff --git a/tests/test_utils.go b/tests/test_utils.go index f16754b056..11e5ac2434 100644 --- a/tests/test_utils.go +++ b/tests/test_utils.go @@ -4,10 +4,7 @@ package tests import ( - "database/sql" - "fmt" "path/filepath" - "strings" "testing" "code.gitea.io/gitea/models/db" @@ -40,97 +37,14 @@ func InitIntegrationTest() error { } setting.LoadDBSetting() - if err := storage.Init(); err != nil { + cleanupDb, err := unittest.ResetTestDatabase() + if err != nil { return err } + _ = cleanupDb // no clean up yet (not really needed at the moment) - switch { - case setting.Database.Type.IsMySQL(): - { - connType := util.Iif(strings.HasPrefix(setting.Database.Host, "/"), "unix", "tcp") - db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@%s(%s)/", - setting.Database.User, setting.Database.Passwd, connType, setting.Database.Host)) - if err != nil { - return err - } - defer db.Close() - if _, err = db.Exec("CREATE DATABASE IF NOT EXISTS " + setting.Database.Name); err != nil { - return err - } - } - case setting.Database.Type.IsPostgreSQL(): - openPostgreSQL := func() (*sql.DB, error) { - if strings.HasPrefix(setting.Database.Host, "/") { - return sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@/%s?sslmode=%s&host=%s", - setting.Database.User, setting.Database.Passwd, setting.Database.Name, setting.Database.SSLMode, setting.Database.Host)) - } - return sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=%s", - setting.Database.User, setting.Database.Passwd, setting.Database.Host, setting.Database.Name, setting.Database.SSLMode)) - } - - // create database - { - db, err := openPostgreSQL() - if err != nil { - return err - } - defer db.Close() - dbRows, err := db.Query(fmt.Sprintf("SELECT 1 FROM pg_database WHERE datname = '%s'", setting.Database.Name)) - if err != nil { - return err - } - defer dbRows.Close() - - if !dbRows.Next() { - if _, err = db.Exec("CREATE DATABASE " + setting.Database.Name); err != nil { - return err - } - } - // Check if we need to set up a specific schema - if setting.Database.Schema == "" { - break - } - db.Close() - } - - // create schema - { - db, err := openPostgreSQL() - if err != nil { - return err - } - defer db.Close() - - schemaRows, err := db.Query(fmt.Sprintf("SELECT 1 FROM information_schema.schemata WHERE schema_name = '%s'", setting.Database.Schema)) - if err != nil { - return err - } - defer schemaRows.Close() - - if !schemaRows.Next() { - // Create and set up a DB schema - if _, err = db.Exec("CREATE SCHEMA " + setting.Database.Schema); err != nil { - return err - } - } - } - - case setting.Database.Type.IsMSSQL(): - { - host, port := setting.ParseMSSQLHostPort(setting.Database.Host) - db, err := sql.Open("mssql", fmt.Sprintf("server=%s; port=%s; database=%s; user id=%s; password=%s;", - host, port, "master", setting.Database.User, setting.Database.Passwd)) - if err != nil { - return err - } - defer db.Close() - if _, err = db.Exec(fmt.Sprintf("If(db_id(N'%s') IS NULL) BEGIN CREATE DATABASE %s; END;", setting.Database.Name, setting.Database.Name)); err != nil { - return err - } - } - case setting.Database.Type.IsSQLite3(): - default: - return fmt.Errorf("unsupported database type: %s", setting.Database.Type) + if err := storage.Init(); err != nil { + return err } routers.InitWebInstalled(graceful.GetManager().HammerContext())