Files
gitea/routers/install/routes_test.go
T
petru 3093e56b88
release-nightly / nightly-binary (push) Has been cancelled
release-nightly / nightly-container (push) Has been cancelled
Step-1 Modified - [install] [backup] [database] [recovery] Added periodic database backups and integrated them into the installer recovery flow.
- 1 - Add: Gitea now creates timestamped database backup bundles under `[backup].PATH`, with `manifest.json`, SQL dump output, optional gzip compression, optional `app.ini` snapshotting, and retention-based cleanup.
- 2 - Mod: the installer now includes a `Database Backup` section under `Database Settings`, reads and writes the `[backup]` and `[cron.database_backup]` settings, and starts from an enabled-by-default daily backup schedule for installs or updates that do not already define backup cron values.
- 3 - Mod: the existing installer recovery modal now works as a recovery-source chooser for existing databases, compatible database backup bundles, or repository-filesystem recovery, with source-specific warnings, confirmations, and preserved modal state across rerenders.
- 4 - Add: installer-side database backup restore now detects compatible bundles from the configured backup path, lets the user choose a bundle, and imports the selected `database.sql` or `database.sql.gz` into the target database during reinstall recovery.
- 5 - Mod: the admin maintenance dashboard and cron monitor now expose the `database_backup` task directly, including the configured schedule even before the task is manually enabled.
- 6 - Fix: backup retention pruning no longer panics when `RETENTION_COUNT` is greater than the number of available backup directories.
2026-05-25 00:17:53 +03:00

543 lines
17 KiB
Go

// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package install
import (
"bytes"
"encoding/json"
"mime/multipart"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/services/forms"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRoutes(t *testing.T) {
defer test.MockVariableValue(&setting.InstallLock, false)()
r := Routes()
assert.NotNil(t, r)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/", nil)
r.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), `class="page-content install`)
assert.Contains(t, w.Body.String(), `name="default_language"`)
assert.Contains(t, w.Body.String(), `name="app_ini_file"`)
w = httptest.NewRecorder()
req = httptest.NewRequest(http.MethodGet, "/no-such", nil)
r.ServeHTTP(w, req)
assert.Equal(t, 404, w.Code)
w = httptest.NewRecorder()
req = httptest.NewRequest(http.MethodGet, "/assets/img/gitea.svg", nil)
r.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
}
func TestReorderInstallLanguages(t *testing.T) {
defer test.MockVariableValue(&setting.Langs, []string{"en-US", "de-DE", "fr-FR"})()
assert.Equal(t, "en-US", resolveInstallDefaultLanguage(""))
assert.Equal(t, "en-US", resolveInstallDefaultLanguage("invalid"))
assert.Equal(t, "fr-FR", resolveInstallDefaultLanguage("fr-FR"))
assert.Equal(t, []string{"fr-FR", "en-US", "de-DE"}, reorderInstallLanguages("fr-FR"))
assert.Equal(t, []string{"en-US", "de-DE", "fr-FR"}, reorderInstallLanguages("invalid"))
}
func TestInstallMailerDisplayName(t *testing.T) {
assert.Equal(t, "gitSafe", installMailerDisplayName("gitSafe: for your code"))
assert.Equal(t, "gitSafe", installMailerDisplayName("gitSafe for your code"))
assert.Equal(t, "Gitea", installMailerDisplayName(""))
}
func TestComposeInstallSMTPFrom(t *testing.T) {
from, err := composeInstallSMTPFrom("gitSafe for your code", "", "noreply@example.com")
require.NoError(t, err)
assert.Equal(t, "gitSafe <noreply@example.com>", from)
}
func TestPopulateInstallFormFromConfig(t *testing.T) {
defer test.MockVariableValue(&setting.SupportedDatabaseTypes, []string{"mysql", "postgres", "sqlite3"})()
defer test.MockVariableValue(&setting.Langs, []string{"en-US", "de-DE", "fr-FR"})()
cfg, err := setting.NewConfigProviderFromData(`
APP_NAME = Imported Gitea
RUN_USER = imported-user
[database]
DB_TYPE = postgres
HOST = db.example.com:5432
USER = gitea
PASSWD = secret
NAME = giteadb
SCHEMA = custom
SSL_MODE = require
[repository]
ROOT = /srv/gitea/repos
ALLOW_ADOPTION_OF_UNADOPTED_REPOSITORIES = false
ALLOW_DELETE_OF_UNADOPTED_REPOSITORIES = true
[server]
DOMAIN = gitea.example.com
SSH_PORT = 2222
HTTP_PORT = 4000
ROOT_URL = https://gitea.example.com/
[lfs]
PATH = /srv/gitea/lfs
[log]
ROOT_PATH = /srv/gitea/log
[mailer]
ENABLED = true
SMTP_ADDR = smtp.example.com
SMTP_PORT = 587
FROM = Gitea <gitea@example.com>
USER = smtp-user
PASSWD = smtp-pass
[service]
REGISTER_EMAIL_CONFIRM = true
REGISTER_MANUAL_CONFIRM = false
ENABLE_NOTIFY_MAIL = true
ADMIN_CREATED_ACCOUNT_MODE = invite
DISABLE_REGISTRATION = false
ALLOW_ONLY_INTERNAL_REGISTRATION = true
ALLOW_ONLY_EXTERNAL_REGISTRATION = false
ENABLE_CAPTCHA = true
REQUIRE_SIGNIN_VIEW = true
DEFAULT_KEEP_EMAIL_PRIVATE = true
DEFAULT_ALLOW_CREATE_ORGANIZATION = false
DEFAULT_ENABLE_TIMETRACKING = false
NO_REPLY_ADDRESS = noreply.example.com
[openid]
ENABLE_OPENID_SIGNIN = true
ENABLE_OPENID_SIGNUP = true
[security]
ENABLE_UPDATE_CHECKER = true
PASSWORD_HASH_ALGO = pbkdf2
[admin]
ADMIN_MANAGEMENT_POLICY = super_admin_only
[repository.release]
MAX_FILES = 20
FILE_MAX_SIZE = 2048
[backup]
PATH = /srv/gitea/backups/db
RETENTION_COUNT = 9
COMPRESS = false
INCLUDE_APP_INI_SNAPSHOT = false
[cron.database_backup]
ENABLED = true
RUN_AT_START = true
SCHEDULE = @every 12h
[i18n]
LANGS = de-DE,en-US
`)
require.NoError(t, err)
form, curDBType := newInstallFormFromSettings()
curDBType = populateInstallFormFromConfig(&form, cfg, curDBType)
assert.Equal(t, "postgres", curDBType)
assert.Equal(t, "postgres", form.DbType)
assert.Equal(t, "Imported Gitea", form.AppName)
assert.Equal(t, "imported-user", form.RunUser)
assert.Equal(t, "db.example.com:5432", form.DbHost)
assert.Equal(t, "gitea", form.DbUser)
assert.Equal(t, "secret", form.DbPasswd)
assert.Equal(t, "giteadb", form.DbName)
assert.Equal(t, "custom", form.DbSchema)
assert.Equal(t, "require", form.SSLMode)
assert.Equal(t, "/srv/gitea/repos", form.RepoRootPath)
assert.False(t, form.AllowAdoptionOfUnadoptedRepositories)
assert.True(t, form.AllowDeleteOfUnadoptedRepositories)
assert.Equal(t, "/srv/gitea/lfs", form.LFSRootPath)
assert.Equal(t, "gitea.example.com", form.Domain)
assert.Equal(t, 2222, form.SSHPort)
assert.Equal(t, "4000", form.HTTPPort)
assert.Equal(t, "https://gitea.example.com/", form.AppURL)
assert.Equal(t, "/srv/gitea/log", form.LogRootPath)
assert.Equal(t, "smtp.example.com", form.SMTPAddr)
assert.Equal(t, "587", form.SMTPPort)
assert.Equal(t, "Gitea <gitea@example.com>", form.SMTPFrom)
assert.Equal(t, "Gitea", form.SMTPFromName)
assert.Equal(t, "gitea@example.com", form.SMTPFromAddress)
assert.Equal(t, "smtp-user", form.SMTPUser)
assert.Equal(t, "smtp-pass", form.SMTPPasswd)
assert.Equal(t, "de-DE", form.DefaultLanguage)
assert.True(t, form.RegisterConfirm)
assert.False(t, form.RegisterManualConfirm)
assert.True(t, form.MailNotify)
assert.Equal(t, "invite", form.AdminCreatedAccountMode)
assert.Equal(t, "local_only", form.RegistrationMode)
assert.False(t, form.EnableOpenIDSignIn)
assert.False(t, form.EnableOpenIDSignUp)
assert.True(t, form.EnableCaptcha)
assert.True(t, form.RequireSignInView)
assert.True(t, form.DefaultKeepEmailPrivate)
assert.False(t, form.DefaultAllowCreateOrganization)
assert.False(t, form.DefaultEnableTimetracking)
assert.Equal(t, "noreply.example.com", form.NoReplyAddress)
assert.True(t, form.EnableUpdateChecker)
assert.Equal(t, "pbkdf2", form.PasswordAlgorithm)
assert.Equal(t, "super_admin_only", form.AdminManagementPolicy)
assert.EqualValues(t, 20, form.ReleaseMaxFiles)
assert.EqualValues(t, 2048, form.ReleaseFileMaxSize)
assert.True(t, form.BackupEnabled)
assert.True(t, form.BackupRunAtStart)
assert.Equal(t, "@every 12h", form.BackupSchedule)
assert.Equal(t, "/srv/gitea/backups/db", form.BackupPath)
assert.Equal(t, 9, form.BackupRetentionCount)
assert.False(t, form.BackupCompress)
assert.False(t, form.BackupIncludeAppINISnapshot)
}
// start edit/add - by petru @ codex
func TestNewInstallFormFromSettingsBackupDefaults(t *testing.T) {
cfg, err := setting.NewConfigProviderFromData("")
require.NoError(t, err)
defer test.MockVariableValue(&setting.CfgProvider, cfg)()
form, _ := newInstallFormFromSettings()
assert.True(t, form.BackupEnabled)
assert.False(t, form.BackupRunAtStart)
assert.Equal(t, "@daily", form.BackupSchedule)
}
// end edit/add - by petru @ codex
// start edit/add - by petru @ codex
func TestApplyImportedAppINIRepositoryRecoveryDefaults(t *testing.T) {
form := forms.InstallForm{
ImportedAppINI: true,
RecoveryMode: recoveryModeRepositoryFileSystem,
AllowAdoptionOfUnadoptedRepositories: false,
AllowDeleteOfUnadoptedRepositories: false,
}
applyImportedAppINIRepositoryRecoveryDefaults(&form, false)
assert.True(t, form.AllowAdoptionOfUnadoptedRepositories)
assert.True(t, form.AllowDeleteOfUnadoptedRepositories)
form = forms.InstallForm{
ImportedAppINI: true,
RecoveryMode: recoveryModeRepositoryFileSystem,
AllowAdoptionOfUnadoptedRepositories: false,
AllowDeleteOfUnadoptedRepositories: false,
}
applyImportedAppINIRepositoryRecoveryDefaults(&form, true)
assert.False(t, form.AllowAdoptionOfUnadoptedRepositories)
assert.False(t, form.AllowDeleteOfUnadoptedRepositories)
}
func TestApplyImportedAppINIRepositoryRecoveryDefaultsWithoutFilesystemRecoveryMode(t *testing.T) {
form := forms.InstallForm{
ImportedAppINI: true,
RecoveryMode: recoveryModeExistingDatabase,
AllowAdoptionOfUnadoptedRepositories: false,
AllowDeleteOfUnadoptedRepositories: false,
}
applyImportedAppINIRepositoryRecoveryDefaults(&form, false)
assert.False(t, form.AllowAdoptionOfUnadoptedRepositories)
assert.False(t, form.AllowDeleteOfUnadoptedRepositories)
}
func TestHasInstallRepositoryFilesystem(t *testing.T) {
tmpDir := t.TempDir()
assert.False(t, hasInstallRepositoryFilesystem(tmpDir))
require.NoError(t, os.Mkdir(filepath.Join(tmpDir, "owner"), 0o755))
require.NoError(t, os.Mkdir(filepath.Join(tmpDir, "owner", "repo.git"), 0o755))
assert.True(t, hasInstallRepositoryFilesystem(tmpDir))
tmpDirFlat := t.TempDir()
require.NoError(t, os.Mkdir(filepath.Join(tmpDirFlat, "repo.git"), 0o755))
assert.True(t, hasInstallRepositoryFilesystem(tmpDirFlat))
}
func TestResolveInstallBackupPath(t *testing.T) {
defer test.MockVariableValue(&setting.AppWorkPath, "/srv/gitea")()
assert.Equal(t, "", resolveInstallBackupPath(""))
assert.Equal(t, "/srv/gitea/data/backups/db", resolveInstallBackupPath("data/backups/db"))
assert.Equal(t, "/var/lib/gitea/backups", resolveInstallBackupPath("/var/lib/gitea/backups"))
}
func TestListInstallBackupChoices(t *testing.T) {
tmpDir := t.TempDir()
writeInstallBackupManifestForTest(t, tmpDir, "20260524-010101.000000001", "sqlite3", 100)
writeInstallBackupManifestForTest(t, tmpDir, "20260524-010102.000000002", "mysql", 200)
writeInstallBackupManifestForTest(t, tmpDir, "20260524-010103.000000003", "sqlite3", 300)
choices, err := listInstallBackupChoices(tmpDir, "sqlite3")
require.NoError(t, err)
require.Len(t, choices, 2)
assert.Equal(t, "20260524-010103.000000003", choices[0].ID)
assert.Equal(t, "20260524-010101.000000001", choices[1].ID)
assert.Equal(t, "20260524-010103.000000003 (sqlite3)", choices[0].Label)
}
// end edit/add - by petru @ codex
// start edit/add - by petru @ codex
func writeInstallBackupManifestForTest(t *testing.T, rootPath, backupID, dbType string, createdUnix int64) {
t.Helper()
backupDir := filepath.Join(rootPath, backupID)
require.NoError(t, os.MkdirAll(backupDir, 0o755))
manifestBytes, err := json.Marshal(map[string]any{
"schema_version": 1,
"id": backupID,
"created_unix": createdUnix,
"db_type": dbType,
"app_version": "dev",
"app_name": "Test",
"migration_version": 1,
"file_name": "database.sql.gz",
"file_sha256": "abc",
"file_size": 1,
"compressed": true,
})
require.NoError(t, err)
require.NoError(t, os.WriteFile(filepath.Join(backupDir, "manifest.json"), manifestBytes, 0o644))
}
// end edit/add - by petru @ codex
func TestPopulateInstallFormFromConfigReplacesSMTPFromSplitFields(t *testing.T) {
cfg, err := setting.NewConfigProviderFromData(`
APP_NAME = gitSafe: for your code
[mailer]
ENABLED = true
SMTP_ADDR = smtp.example.com
SMTP_PORT = 587
FROM = gitSafe <noreply@example.com>
`)
require.NoError(t, err)
form, curDBType := newInstallFormFromSettings()
form.SMTPFrom = "Legacy <legacy@example.com>"
form.SMTPFromName = "Legacy"
form.SMTPFromAddress = "legacy@example.com"
populateInstallFormFromConfig(&form, cfg, curDBType)
assert.Equal(t, "gitSafe <noreply@example.com>", form.SMTPFrom)
assert.Equal(t, "gitSafe", form.SMTPFromName)
assert.Equal(t, "noreply@example.com", form.SMTPFromAddress)
}
func TestPopulateInstallFormFromConfigWithSensitiveSecrets(t *testing.T) {
cfg, err := setting.NewConfigProviderFromData(`
[server]
LFS_JWT_SECRET = lfs-secret
[security]
INTERNAL_TOKEN = internal-secret
[oauth2]
JWT_SECRET = oauth-secret
`)
require.NoError(t, err)
form, curDBType := newInstallFormFromSettings()
form.ImportSensitiveSecrets = true
populateInstallFormFromConfig(&form, cfg, curDBType)
assert.Equal(t, "lfs-secret", form.ImportedLFSJWTSecret)
assert.Equal(t, "internal-secret", form.ImportedInternalToken)
assert.Equal(t, "oauth-secret", form.ImportedOAuth2JWTSecret)
}
func TestPopulateInstallFormFromConfigWithSensitiveSecretURIs(t *testing.T) {
tmpDir := t.TempDir()
lfsSecretPath := filepath.Join(tmpDir, "lfs_secret")
internalTokenPath := filepath.Join(tmpDir, "internal_token")
oauthSecretPath := filepath.Join(tmpDir, "oauth_secret")
require.NoError(t, os.WriteFile(lfsSecretPath, []byte("lfs-secret-uri\n"), 0o644))
require.NoError(t, os.WriteFile(internalTokenPath, []byte("internal-secret-uri\n"), 0o644))
require.NoError(t, os.WriteFile(oauthSecretPath, []byte("oauth-secret-uri\n"), 0o644))
cfg, err := setting.NewConfigProviderFromData(`
[server]
LFS_JWT_SECRET_URI = file:` + filepath.ToSlash(lfsSecretPath) + `
[security]
INTERNAL_TOKEN_URI = file:` + filepath.ToSlash(internalTokenPath) + `
[oauth2]
JWT_SECRET_URI = file:` + filepath.ToSlash(oauthSecretPath) + `
`)
require.NoError(t, err)
form, curDBType := newInstallFormFromSettings()
form.ImportSensitiveSecrets = true
populateInstallFormFromConfig(&form, cfg, curDBType)
assert.Equal(t, "lfs-secret-uri", form.ImportedLFSJWTSecret)
assert.Equal(t, "internal-secret-uri", form.ImportedInternalToken)
assert.Equal(t, "oauth-secret-uri", form.ImportedOAuth2JWTSecret)
}
func TestPopulateInstallSensitiveSecretsFromConfigFillsMissingValuesOnly(t *testing.T) {
tmpDir := t.TempDir()
internalTokenPath := filepath.Join(tmpDir, "internal_token")
require.NoError(t, os.WriteFile(internalTokenPath, []byte("internal-secret-uri\n"), 0o644))
cfg, err := setting.NewConfigProviderFromData(`
[server]
LFS_JWT_SECRET = lfs-secret
[security]
INTERNAL_TOKEN_URI = file:` + filepath.ToSlash(internalTokenPath) + `
[oauth2]
JWT_SECRET = oauth-secret
`)
require.NoError(t, err)
form := forms.InstallForm{
ImportSensitiveSecrets: true,
ImportedLFSJWTSecret: "",
ImportedInternalToken: "",
ImportedOAuth2JWTSecret: "already-set",
}
populateInstallSensitiveSecretsFromConfig(&form, cfg)
assert.Equal(t, "lfs-secret", form.ImportedLFSJWTSecret)
assert.Equal(t, "internal-secret-uri", form.ImportedInternalToken)
assert.Equal(t, "already-set", form.ImportedOAuth2JWTSecret)
}
func TestImportAppINIWithSensitiveSecrets(t *testing.T) {
defer test.MockVariableValue(&setting.InstallLock, false)()
var body bytes.Buffer
writer := multipart.NewWriter(&body)
require.NoError(t, writer.WriteField("import_sensitive_secrets", "on"))
fileWriter, err := writer.CreateFormFile("app_ini_file", "app.ini")
require.NoError(t, err)
_, err = fileWriter.Write([]byte(`
[server]
LFS_JWT_SECRET = lfs-secret
[security]
INTERNAL_TOKEN = internal-secret
[oauth2]
JWT_SECRET = oauth-secret
`))
require.NoError(t, err)
require.NoError(t, writer.Close())
r := Routes()
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/import_app_ini", &body)
req.Header.Set("Content-Type", writer.FormDataContentType())
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), `name="imported_lfs_jwt_secret" value="lfs-secret"`)
assert.Contains(t, w.Body.String(), `name="imported_internal_token" value="internal-secret"`)
assert.Contains(t, w.Body.String(), `name="imported_o_auth2_jwt_secret" value="oauth-secret"`)
}
// start edit/add - by petru @ codex
func TestImportAppINIDefaultsSensitiveSecretsToEnabled(t *testing.T) {
defer test.MockVariableValue(&setting.InstallLock, false)()
var body bytes.Buffer
writer := multipart.NewWriter(&body)
fileWriter, err := writer.CreateFormFile("app_ini_file", "app.ini")
require.NoError(t, err)
_, err = fileWriter.Write([]byte(`
[server]
LFS_JWT_SECRET = lfs-secret
[security]
INTERNAL_TOKEN = internal-secret
[oauth2]
JWT_SECRET = oauth-secret
`))
require.NoError(t, err)
require.NoError(t, writer.Close())
r := Routes()
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/import_app_ini", &body)
req.Header.Set("Content-Type", writer.FormDataContentType())
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), `id="import_sensitive_secrets" name="import_sensitive_secrets" type="checkbox" checked`)
assert.Contains(t, w.Body.String(), `name="imported_lfs_jwt_secret" value="lfs-secret"`)
assert.Contains(t, w.Body.String(), `name="imported_internal_token" value="internal-secret"`)
assert.Contains(t, w.Body.String(), `name="imported_o_auth2_jwt_secret" value="oauth-secret"`)
}
// end edit/add - by petru @ codex
func TestApplyInstallSensitiveSecretsToConfigPersistsImportedValues(t *testing.T) {
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, "app.ini")
defer test.MockVariableValue(&setting.InternalToken, "")()
cfg, err := setting.NewConfigProviderFromData("")
require.NoError(t, err)
form := forms.InstallForm{
LFSRootPath: filepath.Join(tmpDir, "lfs"),
ImportSensitiveSecrets: true,
ImportedLFSJWTSecret: "lfs-secret",
ImportedInternalToken: "internal-secret",
ImportedOAuth2JWTSecret: "oauth-secret",
}
require.NoError(t, applyInstallSensitiveSecretsToConfig(cfg, &form))
require.NoError(t, cfg.SaveTo(configPath))
data, err := os.ReadFile(configPath)
require.NoError(t, err)
content := string(data)
assert.Contains(t, content, "LFS_JWT_SECRET = lfs-secret")
assert.Contains(t, content, "INTERNAL_TOKEN = internal-secret")
assert.Contains(t, content, "JWT_SECRET = oauth-secret")
}
func TestMain(m *testing.M) {
unittest.MainTest(m)
}