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") } }