package config import ( "os" "testing" "github.com/stretchr/testify/assert" "github.com/photoprism/photoprism/internal/service/cluster" ) // resetDatabaseOptions clears all DB-related option fields so tests start from defaults even if // storage/testdata/config/options.yml contains legacy values such as DatabaseDsn. func resetDatabaseOptions(c *Config) { c.options.DatabaseDriver = "" c.options.DatabaseDSN = "" c.options.Deprecated.DatabaseDsn = "" c.options.DatabaseServer = "" c.options.DatabaseName = "" c.options.DatabaseUser = "" c.options.DatabasePassword = "" } func TestConfig_DatabaseDriver(t *testing.T) { t.Run("DefaultsToSQLite", func(t *testing.T) { c := NewConfig(CliTestContext()) resetDatabaseOptions(c) assert.Equal(t, SQLite3, c.DatabaseDriver()) }) t.Run("NormalizesDeprecatedDSN", func(t *testing.T) { c := NewConfig(CliTestContext()) resetDatabaseOptions(c) c.options.DatabaseDriver = MySQL c.options.Deprecated.DatabaseDsn = "user:pass@tcp(localhost:3306)/photoprism" assert.Equal(t, MySQL, c.DatabaseDriver()) assert.Equal(t, "user:pass@tcp(localhost:3306)/photoprism", c.options.DatabaseDSN) assert.Empty(t, c.options.Deprecated.DatabaseDsn) }) } func TestConfig_DatabaseDriverName(t *testing.T) { c := NewConfig(CliTestContext()) resetDatabaseOptions(c) driver := c.DatabaseDriverName() assert.Equal(t, "SQLite", driver) } func TestConfig_DatabaseVersion(t *testing.T) { c := TestConfig() assert.NotEmpty(t, c.DatabaseVersion()) assert.True(t, c.IsDatabaseVersion("v3.45")) } func TestConfig_DatabaseSsl(t *testing.T) { c := TestConfig() assert.False(t, c.DatabaseSsl()) } func TestConfig_normalizeDatabaseDSN(t *testing.T) { c := NewConfig(CliTestContext()) c.options.Deprecated.DatabaseDsn = "foo:b@r@tcp(honeypot:1234)/baz?charset=utf8mb4,utf8&parseTime=true" c.options.DatabaseDriver = MySQL assert.Equal(t, "honeypot:1234", c.DatabaseServer()) assert.Equal(t, "honeypot", c.DatabaseHost()) assert.Equal(t, 1234, c.DatabasePort()) assert.Equal(t, "baz", c.DatabaseName()) assert.Equal(t, "foo", c.DatabaseUser()) assert.Equal(t, "b@r", c.DatabasePassword()) } func TestConfig_ParseDatabaseDSN(t *testing.T) { c := NewConfig(CliTestContext()) c.options.DatabaseDSN = "foo:b@r@tcp(honeypot:1234)/baz?charset=utf8mb4,utf8&parseTime=true" c.options.DatabaseDriver = SQLite3 assert.Equal(t, "", c.DatabaseServer()) assert.Equal(t, "", c.DatabaseHost()) assert.Equal(t, 0, c.DatabasePort()) assert.Equal(t, "foo:b@r@tcp(honeypot:1234)/baz?charset=utf8mb4,utf8&parseTime=true", c.DatabaseName()) assert.Equal(t, "", c.DatabaseUser()) assert.Equal(t, "", c.DatabasePassword()) c.options.DatabaseDriver = MySQL assert.Equal(t, "honeypot:1234", c.DatabaseServer()) assert.Equal(t, "honeypot", c.DatabaseHost()) assert.Equal(t, 1234, c.DatabasePort()) assert.Equal(t, "baz", c.DatabaseName()) assert.Equal(t, "foo", c.DatabaseUser()) assert.Equal(t, "b@r", c.DatabasePassword()) c.options.DatabaseDriver = SQLite3 assert.Equal(t, "", c.DatabaseServer()) assert.Equal(t, "", c.DatabaseHost()) assert.Equal(t, 0, c.DatabasePort()) assert.Equal(t, "foo:b@r@tcp(honeypot:1234)/baz?charset=utf8mb4,utf8&parseTime=true", c.DatabaseName()) assert.Equal(t, "", c.DatabaseUser()) assert.Equal(t, "", c.DatabasePassword()) t.Run("ManualServerConfig", func(t *testing.T) { target := NewConfig(CliTestContext()) resetDatabaseOptions(target) target.options.DatabaseDriver = MySQL target.options.DatabaseServer = "db.internal:3306" target.options.DatabaseName = "photoprism" target.options.DatabaseUser = "app" target.options.DatabasePassword = "secret" target.options.DatabaseDSN = "foo:b@r@tcp(otherhost:3307)/other?charset=utf8mb4,utf8&parseTime=true" target.ParseDatabaseDSN() assert.Equal(t, "otherhost:3307", target.options.DatabaseServer) assert.Equal(t, "otherhost", target.DatabaseHost()) assert.Equal(t, "other", target.options.DatabaseName) assert.Equal(t, "foo", target.options.DatabaseUser) assert.Equal(t, "b@r", target.options.DatabasePassword) }) t.Run("SQLiteSkipWhenServerPreset", func(t *testing.T) { cfg := NewConfig(CliTestContext()) resetDatabaseOptions(cfg) cfg.options.DatabaseDriver = SQLite3 cfg.options.DatabaseDSN = "file:/data/app.db?_busy_timeout=5000" cfg.options.DatabaseServer = "/tmp/mysql.sock" cfg.options.DatabaseName = "existing-name" cfg.options.DatabaseUser = "existing-user" cfg.options.DatabasePassword = "existing-pass" cfg.ParseDatabaseDSN() assert.Equal(t, "/tmp/mysql.sock", cfg.options.DatabaseServer) assert.Equal(t, "existing-name", cfg.options.DatabaseName) assert.Equal(t, "existing-user", cfg.options.DatabaseUser) assert.Equal(t, "existing-pass", cfg.options.DatabasePassword) }) } func TestConfig_DatabaseServer(t *testing.T) { c := NewConfig(CliTestContext()) resetDatabaseOptions(c) assert.Equal(t, "", c.DatabaseServer()) c.options.DatabaseServer = "test" assert.Equal(t, "", c.DatabaseServer()) } func TestConfig_DatabaseHost(t *testing.T) { c := NewConfig(CliTestContext()) resetDatabaseOptions(c) assert.Equal(t, "", c.DatabaseHost()) } func TestConfig_DatabasePort(t *testing.T) { c := NewConfig(CliTestContext()) resetDatabaseOptions(c) assert.Equal(t, 0, c.DatabasePort()) } func TestConfig_DatabasePortString(t *testing.T) { c := NewConfig(CliTestContext()) resetDatabaseOptions(c) assert.Equal(t, "", c.DatabasePortString()) } func TestConfig_DatabaseName(t *testing.T) { c := NewConfig(CliTestContext()) resetDatabaseOptions(c) assert.Equal(t, ProjectRoot+"/storage/testdata/index.db?_busy_timeout=5000", c.DatabaseName()) } func TestConfig_DatabaseUser(t *testing.T) { c := NewConfig(CliTestContext()) resetDatabaseOptions(c) assert.Equal(t, "", c.DatabaseUser()) } func TestConfig_DatabasePassword(t *testing.T) { c := NewConfig(CliTestContext()) resetDatabaseOptions(c) assert.Equal(t, "", c.DatabasePassword()) // Test setting the password via secret file. _ = os.Setenv(FlagFileVar("DATABASE_PASSWORD"), "testdata/secret_database") assert.Equal(t, "", c.DatabasePassword()) c.Options().DatabaseDriver = MySQL assert.Equal(t, "StoryOfAmélie", c.DatabasePassword()) c.Options().DatabaseDriver = SQLite3 _ = os.Setenv(FlagFileVar("DATABASE_PASSWORD"), "") assert.Equal(t, "", c.DatabasePassword()) } func TestDatabaseProvisionPrefix(t *testing.T) { t.Run("Default", func(t *testing.T) { conf := NewConfig(CliTestContext()) resetDatabaseOptions(conf) assert.Equal(t, cluster.DefaultDatabaseProvisionPrefix, conf.DatabaseProvisionPrefix()) }) t.Run("SanitizeAndTrim", func(t *testing.T) { conf := NewConfig(CliTestContext()) resetDatabaseOptions(conf) conf.options.DatabaseProvisionPrefix = " My Custom-Prefix!! " got := conf.DatabaseProvisionPrefix() assert.Equal(t, "my_custom_prefix", got) assert.LessOrEqual(t, len(got), cluster.DatabaseProvisionPrefixMaxLen) assert.Equal(t, got, conf.options.DatabaseProvisionPrefix) }) } func TestShouldAutoRotateDatabase(t *testing.T) { t.Run("PortalAlwaysFalse", func(t *testing.T) { conf := NewMinimalTestConfig(t.TempDir()) conf.Options().NodeRole = cluster.RolePortal conf.Options().DatabaseDriver = MySQL assert.False(t, conf.ShouldAutoRotateDatabase()) }) t.Run("NonMySQLDriverFalse", func(t *testing.T) { conf := NewMinimalTestConfig(t.TempDir()) conf.Options().DatabaseDriver = SQLite3 assert.False(t, conf.ShouldAutoRotateDatabase()) }) t.Run("MySQLMissingFieldsTrue", func(t *testing.T) { conf := NewMinimalTestConfig(t.TempDir()) conf.Options().DatabaseDriver = MySQL conf.Options().DatabaseName = "photoprism" conf.Options().DatabaseUser = "" conf.Options().DatabasePassword = "" assert.True(t, conf.ShouldAutoRotateDatabase()) }) } func TestConfig_DatabaseDSN(t *testing.T) { c := NewConfig(CliTestContext()) resetDatabaseOptions(c) driver := c.DatabaseDriver() assert.Equal(t, SQLite3, driver) c.options.DatabaseDSN = "" c.options.DatabaseDriver = "MariaDB" assert.Equal(t, "photoprism:@tcp(localhost)/photoprism?charset=utf8mb4,utf8&collation=utf8mb4_unicode_ci&parseTime=true&timeout=15s", c.DatabaseDSN()) c.options.DatabaseDriver = "tidb" assert.Equal(t, ProjectRoot+"/storage/testdata/index.db?_busy_timeout=5000", c.DatabaseDSN()) c.options.DatabaseDriver = "Postgres" assert.Equal(t, ProjectRoot+"/storage/testdata/index.db?_busy_timeout=5000", c.DatabaseDSN()) c.options.DatabaseDriver = "SQLite" assert.Equal(t, ProjectRoot+"/storage/testdata/index.db?_busy_timeout=5000", c.DatabaseDSN()) c.options.DatabaseDriver = "" assert.Equal(t, ProjectRoot+"/storage/testdata/index.db?_busy_timeout=5000", c.DatabaseDSN()) t.Run("CustomServer", func(t *testing.T) { conf := NewConfig(CliTestContext()) resetDatabaseOptions(conf) conf.options.DatabaseDriver = MySQL conf.options.DatabaseServer = "proxy.internal:6032" conf.options.DatabaseName = "tenantdb" conf.options.DatabaseUser = "tenant" conf.options.DatabasePassword = "secret" conf.options.DatabaseTimeout = 42 want := "tenant:secret@tcp(proxy.internal:6032)/tenantdb?charset=utf8mb4,utf8&collation=utf8mb4_unicode_ci&parseTime=true&timeout=42s" if got := conf.DatabaseDSN(); got != want { t.Fatalf("DatabaseDSN() = %q, want %q", got, want) } }) t.Run("UnixSocket", func(t *testing.T) { conf := NewConfig(CliTestContext()) resetDatabaseOptions(conf) conf.options.DatabaseDriver = MySQL conf.options.DatabaseServer = "/var/run/mysql.sock" conf.options.DatabaseName = "tenantdb" conf.options.DatabaseUser = "tenant" conf.options.DatabasePassword = "secret" conf.options.DatabaseTimeout = 21 want := "tenant:secret@unix(/var/run/mysql.sock)/tenantdb?charset=utf8mb4,utf8&collation=utf8mb4_unicode_ci&parseTime=true&timeout=21s" if got := conf.DatabaseDSN(); got != want { t.Fatalf("DatabaseDSN() = %q, want %q", got, want) } }) } func TestConfig_DatabaseDSNFlags(t *testing.T) { t.Run("NoDatabaseDSN", func(t *testing.T) { conf := NewConfig(CliTestContext()) resetDatabaseOptions(conf) assert.True(t, conf.NoDatabaseDSN()) assert.False(t, conf.HasDatabaseDSN()) }) t.Run("DeprecatedDatabaseDsn", func(t *testing.T) { conf := NewConfig(CliTestContext()) resetDatabaseOptions(conf) conf.options.DatabaseDriver = MySQL conf.options.Deprecated.DatabaseDsn = "user:pass@tcp(db.internal:3306)/photoprism" assert.False(t, conf.NoDatabaseDSN()) assert.True(t, conf.HasDatabaseDSN()) assert.Equal(t, "user:pass@tcp(db.internal:3306)/photoprism?charset=utf8mb4,utf8&collation=utf8mb4_unicode_ci&parseTime=true&timeout=15s", conf.DatabaseDSN()) assert.Empty(t, conf.options.Deprecated.DatabaseDsn) }) } func TestConfig_ReportDatabaseDSN(t *testing.T) { conf := NewConfig(CliTestContext()) resetDatabaseOptions(conf) assert.Equal(t, SQLite3, conf.DatabaseDriver()) assert.True(t, conf.ReportDatabaseDSN()) conf.options.DatabaseDriver = MySQL conf.options.DatabaseDSN = "" assert.False(t, conf.ReportDatabaseDSN()) conf.options.DatabaseDSN = "user:pass@tcp(db.internal:3306)/photoprism" assert.True(t, conf.ReportDatabaseDSN()) } func TestConfig_DatabaseFile(t *testing.T) { c := NewConfig(CliTestContext()) // Ensure SQLite defaults resetDatabaseOptions(c) driver := c.DatabaseDriver() assert.Equal(t, SQLite3, driver) c.options.DatabaseDSN = "" assert.Equal(t, ProjectRoot+"/storage/testdata/index.db", c.DatabaseFile()) assert.Equal(t, ProjectRoot+"/storage/testdata/index.db?_busy_timeout=5000", c.DatabaseDSN()) } func TestConfig_DatabaseTimeout(t *testing.T) { c := NewConfig(CliTestContext()) assert.Equal(t, 15, c.DatabaseTimeout()) c.options.DatabaseTimeout = 1 assert.Equal(t, 1, c.DatabaseTimeout()) c.options.DatabaseTimeout = -1 assert.Equal(t, 15, c.DatabaseTimeout()) c.options.DatabaseTimeout = 120 assert.Equal(t, 60, c.DatabaseTimeout()) c.options.DatabaseTimeout = 0 assert.Equal(t, 15, c.DatabaseTimeout()) c.options.DatabaseTimeout = 15 assert.Equal(t, 15, c.DatabaseTimeout()) } func TestConfig_DatabaseConns(t *testing.T) { c := NewConfig(CliTestContext()) c.options.DatabaseConns = 28 assert.Equal(t, 28, c.DatabaseConns()) c.options.DatabaseConns = 3000 assert.Equal(t, 1024, c.DatabaseConns()) } func TestConfig_DatabaseConnsIdle(t *testing.T) { c := NewConfig(CliTestContext()) c.options.DatabaseConnsIdle = 14 c.options.DatabaseConns = 28 assert.Equal(t, 14, c.DatabaseConnsIdle()) c.options.DatabaseConnsIdle = -55 assert.Greater(t, c.DatabaseConnsIdle(), 8) c.options.DatabaseConnsIdle = 35 assert.Equal(t, 28, c.DatabaseConnsIdle()) } func TestConfig_checkDb(t *testing.T) { c := NewConfig(CliTestContext()) t.Setenv("PHOTOPRISM_DATABASE_SKIP_VERSION_CHECK", "true") assert.NoError(t, c.checkDb(nil)) t.Setenv("PHOTOPRISM_DATABASE_SKIP_VERSION_CHECK", "") assert.Error(t, c.checkDb(nil)) }