471cfdd161
- 1 - Add: Gitea now creates timestamped database backup bundles under `[backup].PATH`, exposes the backup schedule in the installer, and surfaces the `database_backup` cron task in admin monitoring. - 2 - Add: installed instances now use `.gitea-installed` and `.gitea-recovery.ini` to enter email-gated recovery instead of falling back to public install mode when configuration or database access is broken. - 3 - Mod: the installer recovery flow now covers backup-bundle restore, bundled or manual `app.ini` reuse, uploaded SQL/GZ database restores, and repository-filesystem recovery with source-specific validation, confirmations, and preserved launcher state. - 4 - Fix: recovery now restores bundled `app.ini` snapshots when needed, discovers backup bundles from both the active backup path and persisted `.gitea-recovery.ini` path, and preserves SMTP and other rebuilt settings correctly when `app.ini` is missing or incomplete. - 5 - Fix: recovery validation and restore handling now accept either a selected backup bundle or an uploaded SQL/GZ dump, keep sensitive secrets and existing `LFS_JWT_SECRET` when appropriate, clear SQLite restore targets before import, and complete the post-install handoff without redirect loops. - 6 - Mod: fresh installs now default recovery email authorization to enabled with first-admin fallback, and the install/recovery UI, styling, and EN/RO wording were refined to match the final launcher behavior. Co-Authored-By: petru @ codex (GPT-5) <codex@openai.com> (cherry picked from commit 9879caf2292691b0cb521d12e6fee924b066bae2)
147 lines
4.8 KiB
Go
147 lines
4.8 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
user_model "code.gitea.io/gitea/models/user"
|
|
"code.gitea.io/gitea/modules/setting"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// start edit/add - by petru @ codex
|
|
func TestMissingInstalledSQLiteDatabase(t *testing.T) {
|
|
oldDatabase := setting.Database
|
|
oldInstallLock := setting.InstallLock
|
|
oldCustomConf := setting.CustomConf
|
|
defer func() {
|
|
setting.Database = oldDatabase
|
|
setting.InstallLock = oldInstallLock
|
|
setting.CustomConf = oldCustomConf
|
|
}()
|
|
|
|
tmpDir := t.TempDir()
|
|
setting.CustomConf = filepath.Join(tmpDir, "custom", "conf", "app.ini")
|
|
setting.Database.Type = "sqlite3"
|
|
setting.Database.Path = filepath.Join(tmpDir, "data", "gitea.db")
|
|
setting.InstallLock = false // edit/add - by petru @ codex
|
|
|
|
missing, err := missingInstalledSQLiteDatabase()
|
|
require.NoError(t, err)
|
|
assert.False(t, missing)
|
|
|
|
setting.InstallLock = true
|
|
missing, err = missingInstalledSQLiteDatabase()
|
|
require.NoError(t, err)
|
|
assert.True(t, missing)
|
|
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(setting.Database.Path), 0o755))
|
|
require.NoError(t, os.WriteFile(setting.Database.Path, []byte("db"), 0o600))
|
|
missing, err = missingInstalledSQLiteDatabase()
|
|
require.NoError(t, err)
|
|
assert.False(t, missing)
|
|
|
|
require.NoError(t, os.Remove(setting.Database.Path))
|
|
setting.InstallLock = false
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(setting.InstallSentinelPath()), 0o755))
|
|
require.NoError(t, os.WriteFile(setting.InstallSentinelPath(), []byte("installed\n"), 0o600))
|
|
missing, err = missingInstalledSQLiteDatabase()
|
|
require.NoError(t, err)
|
|
assert.True(t, missing)
|
|
|
|
setting.Database.Type = "postgres"
|
|
missing, err = missingInstalledSQLiteDatabase()
|
|
require.NoError(t, err)
|
|
assert.False(t, missing)
|
|
}
|
|
|
|
func TestBuildAutoRecoveryConfigFromInstalledState(t *testing.T) {
|
|
adminUser, adminErr := user_model.GetAdminUser(context.Background())
|
|
recoveryCfg, err := buildAutoRecoveryConfigFromInstalledState(context.Background())
|
|
require.NoError(t, err)
|
|
if adminErr != nil {
|
|
require.True(t, user_model.IsErrUserNotExist(adminErr))
|
|
assert.Nil(t, recoveryCfg)
|
|
return
|
|
}
|
|
require.NotNil(t, adminUser)
|
|
if adminUser.Email == "" {
|
|
assert.Nil(t, recoveryCfg)
|
|
return
|
|
}
|
|
require.NotNil(t, recoveryCfg)
|
|
assert.True(t, recoveryCfg.Enabled)
|
|
assert.Equal(t, adminUser.Email, recoveryCfg.AllowedEmails)
|
|
assert.Equal(t, setting.Backup.Path, recoveryCfg.BackupPath)
|
|
assert.Equal(t, setting.RepoRootPath, recoveryCfg.RepoRootPath)
|
|
if setting.MailService != nil {
|
|
assert.Equal(t, setting.MailService.SMTPAddr, recoveryCfg.SMTPAddr)
|
|
assert.Equal(t, setting.MailService.SMTPPort, recoveryCfg.SMTPPort)
|
|
assert.Equal(t, setting.MailService.From, recoveryCfg.SMTPFrom)
|
|
assert.Equal(t, setting.MailService.User, recoveryCfg.SMTPUser)
|
|
assert.Equal(t, setting.MailService.Passwd, recoveryCfg.SMTPPasswd)
|
|
}
|
|
}
|
|
|
|
func TestShouldServeInstalledRecoveryWithMissingSQLite(t *testing.T) {
|
|
oldDatabase := setting.Database
|
|
oldInstallLock := setting.InstallLock
|
|
oldCustomConf := setting.CustomConf
|
|
defer func() {
|
|
setting.Database = oldDatabase
|
|
setting.InstallLock = oldInstallLock
|
|
setting.CustomConf = oldCustomConf
|
|
}()
|
|
|
|
tmpDir := t.TempDir()
|
|
setting.CustomConf = filepath.Join(tmpDir, "custom", "conf", "app.ini")
|
|
setting.Database.Type = "sqlite3"
|
|
setting.Database.Path = filepath.Join(tmpDir, "data", "gitea.db")
|
|
setting.InstallLock = true
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(setting.InstallSentinelPath()), 0o755))
|
|
require.NoError(t, os.WriteFile(setting.InstallSentinelPath(), []byte("installed\n"), 0o600))
|
|
require.NoError(t, setting.SaveRecoveryConfig(&setting.RecoveryConfig{
|
|
Enabled: true,
|
|
AllowedEmails: "admin@example.com",
|
|
SMTPAddr: "smtp.example.com",
|
|
SMTPPort: "587",
|
|
SMTPFrom: "Gitea <noreply@example.com>",
|
|
}))
|
|
|
|
recoveryCfg, shouldServeRecovery, err := shouldServeInstalledRecovery(context.Background())
|
|
require.NoError(t, err)
|
|
require.True(t, shouldServeRecovery)
|
|
require.NotNil(t, recoveryCfg)
|
|
assert.Equal(t, "admin@example.com", recoveryCfg.AllowedEmails)
|
|
}
|
|
|
|
func TestRecoveryRequestBaseURLStripsRecoveryQueryFromAppURL(t *testing.T) {
|
|
// start edit/add - by petru @ codex
|
|
oldInstallLock := setting.InstallLock
|
|
oldAppURL := setting.AppURL
|
|
oldAppSubURL := setting.AppSubURL
|
|
defer func() {
|
|
setting.InstallLock = oldInstallLock
|
|
setting.AppURL = oldAppURL
|
|
setting.AppSubURL = oldAppSubURL
|
|
}()
|
|
|
|
setting.InstallLock = true
|
|
setting.AppURL = "http://example.com:3000/?recovery=1"
|
|
setting.AppSubURL = ""
|
|
|
|
req := httptest.NewRequest("GET", "http://example.com:3000/recovery", nil)
|
|
assert.Equal(t, "http://example.com:3000", recoveryRequestBaseURL(req))
|
|
// end edit/add - by petru @ codex
|
|
}
|
|
|
|
// end edit/add - by petru @ codex
|