Smail/backend/config/partitioned_compatibility_test.go
smail 0a7167eac0
Some checks are pending
Security Checks / security (push) Waiting to run
修复初始化分区配置代次同步
同步初始化候选配置的分区 generation,避免数据库设置提交被分区代次校验拒绝,并补充回归测试。
2026-09-29 22:54:17 +08:00

145 lines
5.2 KiB
Go

package config
import (
"bytes"
"net/url"
"os"
"path/filepath"
"strings"
"testing"
"time"
"git.pcux.cn/xuans/Smail/utils/credential"
"github.com/go-sql-driver/mysql"
)
func TestPartitionedDatabaseConnectionPreservesMigrationTimezone(t *testing.T) {
vault, err := credential.NewVault(t.TempDir())
if err != nil {
t.Fatal(err)
}
secret, err := ApplySecretChange(vault, StoredSecret{}, SecretChange{Operation: SecretReplace, Value: &SecretInput{Plaintext: "test-password"}}, SecretPolicy{Required: true, MaxLength: 4096}, time.Now().UTC())
if err != nil {
t.Fatal(err)
}
for _, kind := range []DatabaseType{DatabaseTypeMySQL, DatabaseTypePostgreSQL} {
_, dsn, err := compatibilityDatabase(DatabaseConfig{Type: kind, Host: "::1", Port: 3306, Username: "test", Name: "test", Password: secret}, vault)
if err != nil {
t.Fatal("runtime connection failed")
}
if kind == DatabaseTypeMySQL {
settings, err := mysql.ParseDSN(dsn)
_, rawQuery, _ := strings.Cut(dsn, "?")
query, queryErr := url.ParseQuery(rawQuery)
// charset 会被驱动解析为内部选项,不再保留在 Params 中。
if err != nil || queryErr != nil || !settings.ParseTime || settings.Params["time_zone"] != "'+00:00'" || query.Get("charset") != "utf8mb4" {
t.Fatal("MySQL runtime timezone differs from migration")
}
} else {
uri, err := url.Parse(dsn)
if err != nil || uri.Query().Get("timezone") != "UTC" || uri.Query().Get("search_path") != "public" || uri.Query().Get("sslmode") != "require" {
t.Fatal("PostgreSQL runtime options differ from migration")
}
}
}
}
func TestSetupRuntimeFromConfigSynchronizesSectionGeneration(t *testing.T) {
root := t.TempDir()
oldRoot := ROOT_PATH
ROOT_PATH = root
t.Cleanup(func() { ROOT_PATH = oldRoot })
current := validRuntimeConfig()
cfg := &Config{
SetupState: SetupStateBootstrap,
DbType: DBTypeSQLite,
DbDSN: "smail.db",
Domain: "mail.example.com",
Domains: []string{"mail.example.com"},
WebDomain: "mail.example.com",
SSLType: "0",
HttpsEnabled: 1,
HttpPort: 80,
HttpsPort: 443,
}
candidate, err := setupRuntimeFromConfig(current, cfg)
if err != nil {
t.Fatal(err)
}
if candidate.Generation != current.Generation+1 {
t.Fatalf("candidate generation = %d, want %d", candidate.Generation, current.Generation+1)
}
for section, generation := range map[Section]uint64{
SectionSMTP: candidate.SMTP.Meta.Generation,
SectionEmail: candidate.Email.Meta.Generation,
SectionSSL: candidate.SSL.Meta.Generation,
SectionPlugins: candidate.Plugins.Meta.Generation,
} {
if generation != candidate.Generation {
t.Fatalf("%s generation = %d, want %d", section, generation, candidate.Generation)
}
if candidate.Root.Sections[section].Generation != candidate.Generation {
t.Fatalf("%s reference generation = %d, want %d", section, candidate.Root.Sections[section].Generation, candidate.Generation)
}
}
}
func TestPartitionedCompatibilityStartupAndWriteProtection(t *testing.T) {
root := t.TempDir()
oldRoot, oldArgs, oldDevelopment := ROOT_PATH, os.Args, DevelopmentMode
oldInstance, oldSnapshot := Instance, CurrentConfig()
ROOT_PATH, os.Args = root, []string{"smail"}
t.Cleanup(func() {
ROOT_PATH, os.Args, DevelopmentMode = oldRoot, oldArgs, oldDevelopment
Instance = oldInstance
PublishConfigSnapshot(oldSnapshot)
})
writeLegacyMigrationFixture(t, root, map[string]any{
"logLevel": "info", "domain": "mail.example.com", "webDomain": "mail.example.com", "adminRecoveryDomain": "recovery.mail.example.com",
"dbType": "sqlite", "dbDSN": "./config/existing.db", "isInit": true,
"sslType": "1", "SSLPublicKeyPath": "./config/ssl/site.crt", "SSLPrivateKeyPath": "./config/ssl/site.key",
"httpPort": 80, "httpsPort": 443, "httpsEnabled": 1,
"publicHttpsPort": 9443,
})
if _, err := MigrateLegacyConfig(root, RuntimeValidationOptions{HasSuperAdmin: true, Production: true}); err != nil {
t.Fatal(err)
}
path := filepath.Join(root, "config", "config.json")
before, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
for i := 0; i < 2; i++ {
if err := Init(); err != nil {
t.Fatal(err)
}
cfg := CurrentConfig()
if cfg.PublicHTTPSPort != 9443 || cfg.HttpsPort != 443 {
t.Fatal("分区启动兼容快照丢失公网或内部端口")
}
if !cfg.IsSetupReady() || cfg.DbType != DBTypeSQLite || cfg.DbDSN != filepath.Join(root, "data", "existing.db") {
t.Fatal("partitioned startup lost initialization or database identity")
}
if cfg.WebDomain != "mail.example.com" || cfg.AdminRecoveryDomain != "recovery.mail.example.com" || cfg.SSL.CertificateRef != "data/ssl/site.crt" {
t.Fatal("partitioned startup lost domain or certificate path")
}
}
cfg, err := ReadConfig()
if err != nil {
t.Fatal(err)
}
if err := WriteConfig(cfg); err == nil {
t.Fatal("legacy writer must not overwrite partitioned config")
}
after, err := os.ReadFile(path)
if err != nil || !bytes.Equal(before, after) {
t.Fatal("loading or legacy writing changed partitioned root")
}
if err := os.WriteFile(filepath.Join(root, "config", "smtp.json"), []byte("{}"), 0600); err != nil {
t.Fatal(err)
}
if err := Init(); err == nil {
t.Fatal("corrupted partition must not silently fall back to legacy startup")
}
}