package config import ( "os" "path/filepath" "strings" "testing" "time" gc "github.com/patrickmn/go-cache" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/rnd" ) func TestConfig_FindBin(t *testing.T) { assert.Equal(t, "", FindBin("yyy123", "xxx123")) assert.Equal(t, "", FindBin("yyy123", "sh")) assert.Equal(t, "/usr/bin/sh", FindBin("sh", "yyy123")) assert.Equal(t, "/usr/bin/sh", FindBin("", "sh")) assert.Equal(t, "/usr/bin/sh", FindBin("", "", "sh")) assert.Equal(t, "/usr/bin/sh", FindBin("", "yyy123", "sh")) assert.Equal(t, "/usr/bin/sh", FindBin("sh", "bash")) assert.Equal(t, "/usr/bin/bash", FindBin("bash", "sh")) } func TestConfig_SidecarPath(t *testing.T) { c := NewConfig(CliTestContext()) assert.Contains(t, c.SidecarPath(), "testdata/sidecar") c.options.SidecarPath = ".photoprism" assert.Equal(t, ".photoprism", c.SidecarPath()) c.options.SidecarPath = "" assert.Equal(t, ProjectRoot+"/storage/testdata/sidecar", c.SidecarPath()) } func TestConfig_SidecarYaml(t *testing.T) { c := NewConfig(NewTestContext(nil)) // t.Logf("c.options.DisableBackups = %t", c.options.DisableBackups) // t.Logf("c.options.SidecarYaml = %t", c.options.SidecarYaml) assert.Equal(t, true, c.SidecarYaml()) assert.Equal(t, c.DisableBackups(), !c.SidecarYaml()) c.options.DisableBackups = true assert.Equal(t, false, c.SidecarYaml()) assert.Equal(t, c.DisableBackups(), !c.SidecarYaml()) c.options.DisableBackups = false c.options.SidecarYaml = true assert.Equal(t, true, c.SidecarYaml()) assert.Equal(t, c.DisableBackups(), !c.SidecarYaml()) } func TestConfig_UsersPath(t *testing.T) { c := NewConfig(CliTestContext()) assert.Contains(t, c.UsersPath(), "users") } func TestConfig_UsersOriginalsPath(t *testing.T) { c := NewConfig(CliTestContext()) assert.Contains(t, c.UsersOriginalsPath(), "users") } func TestConfig_UsersStoragePath(t *testing.T) { c := NewConfig(CliTestContext()) assert.Contains(t, c.UsersStoragePath(), "users") } func TestConfig_UserStoragePath(t *testing.T) { c := NewConfig(CliTestContext()) assert.Equal(t, "", c.UserStoragePath("")) assert.Equal(t, "", c.UserStoragePath("etaetyget")) assert.Contains(t, c.UserStoragePath("urjult03ceelhw6k"), "users/urjult03ceelhw6k") } func TestConfig_UserUploadPath(t *testing.T) { c := NewConfig(CliTestContext()) if dir, err := c.UserUploadPath("", ""); err == nil { t.Error("error expected") } else { assert.Equal(t, "", dir) } if dir, err := c.UserUploadPath("etaetyget", ""); err == nil { t.Error("error expected") } else { assert.Equal(t, "", dir) } if dir, err := c.UserUploadPath("urjult03ceelhw6k", ""); err != nil { t.Fatal(err) } else { assert.Contains(t, dir, "users/urjult03ceelhw6k/upload") } if dir, err := c.UserUploadPath("urjult03ceelhw6k", "foo"); err != nil { t.Fatal(err) } else { assert.Contains(t, dir, "users/urjult03ceelhw6k/upload/foo") } } func TestConfig_SidecarPathIsAbs(t *testing.T) { c := NewConfig(CliTestContext()) assert.Equal(t, true, c.SidecarPathIsAbs()) c.options.SidecarPath = ".photoprism" assert.Equal(t, false, c.SidecarPathIsAbs()) } func TestConfig_SidecarWritable(t *testing.T) { c := NewConfig(CliTestContext()) assert.Equal(t, true, c.SidecarWritable()) } func TestConfig_FFmpegBin(t *testing.T) { c := NewConfig(CliTestContext()) assert.True(t, strings.Contains(c.FFmpegBin(), "/bin/ffmpeg")) } func TestConfig_FFprobeBin(t *testing.T) { c := NewConfig(CliTestContext()) assert.True(t, strings.Contains(c.FFprobeBin(), "/bin/ffprobe")) } func TestConfig_YtDlpBin(t *testing.T) { c := NewConfig(CliTestContext()) assert.True(t, strings.Contains(c.YtDlpBin(), "/bin/yt-dlp")) } func TestConfig_TempPath(t *testing.T) { c := NewConfig(CliTestContext()) d0 := c.tempPath() t.Logf("c.options.TempPath: '%s'", c.options.TempPath) t.Logf("c.tempPath(): '%s'", d0) assert.Equal(t, ProjectRoot+"/storage/testdata/temp", c.tempPath()) c.options.TempPath = "" d1 := c.tempPath() if d1 == "" { t.Fatal("temp path is empty") } if !strings.HasPrefix(d1, "/tmp/photoprism_") { t.Fatalf("unexpected temp path: %s", d1) } d2 := c.tempPath() if d2 == "" { t.Fatal("temp path is empty") } if !strings.HasPrefix(d2, "/tmp/photoprism_") { t.Fatalf("unexpected temp path: %s", d2) } if d1 != d2 { t.Fatalf("temp paths should match: '%s' <=> '%s'", d1, d2) } else { t.Logf("temp paths match: '%s' == '%s'", d1, d2) } if d4 := c.TempPath(); d4 != d0 { t.Fatalf("temp paths should match: '%s' <=> '%s'", d4, d0) } else { t.Logf("temp paths match: '%s' == '%s'", d4, d0) } } func TestConfig_CmdCachePath(t *testing.T) { c := NewConfig(CliTestContext()) if dir := c.CmdCachePath(); dir == "" { t.Fatal("cmd cache path is empty") } else if !strings.HasPrefix(dir, c.CachePath()) { t.Fatalf("unexpected cmd cache path: %s", dir) } } func TestConfig_CmdLibPath(t *testing.T) { c := NewConfig(CliTestContext()) if dir := c.CmdLibPath(); dir == "" { t.Fatal("cmd lib path is empty") } else if !strings.HasPrefix(dir, "/usr") { t.Fatalf("unexpected cmd lib path: %s", dir) } } func TestConfig_CachePath2(t *testing.T) { c := NewConfig(CliTestContext()) assert.Equal(t, ProjectRoot+"/storage/testdata/cache", c.CachePath()) c.options.CachePath = "" assert.Equal(t, ProjectRoot+"/storage/testdata/cache", c.CachePath()) } func TestConfig_SettingsYaml(t *testing.T) { t.Run("Default", func(t *testing.T) { c := NewConfig(CliTestContext()) assert.Contains(t, c.SettingsYaml(), "settings.yml") }) t.Run("PreferYamlExtension", func(t *testing.T) { c := NewConfig(CliTestContext()) tempDir := t.TempDir() c.options.ConfigPath = tempDir yamlPath := filepath.Join(tempDir, "settings"+fs.ExtYaml) if err := os.WriteFile(yamlPath, []byte("ui:\n"), fs.ModeFile); err != nil { t.Fatalf("write %s: %v", yamlPath, err) } assert.Equal(t, yamlPath, c.SettingsYaml()) }) } func TestConfig_HubConfigFile(t *testing.T) { t.Run("Default", func(t *testing.T) { c := NewConfig(CliTestContext()) assert.Contains(t, c.HubConfigFile(), "hub.yml") }) t.Run("PreferYamlExtension", func(t *testing.T) { c := NewConfig(CliTestContext()) tempDir := t.TempDir() c.options.ConfigPath = tempDir yamlPath := filepath.Join(tempDir, "hub"+fs.ExtYaml) if err := os.WriteFile(yamlPath, []byte("host: example\n"), fs.ModeFile); err != nil { t.Fatalf("write %s: %v", yamlPath, err) } assert.Equal(t, yamlPath, c.HubConfigFile()) }) } func TestConfig_StoragePath(t *testing.T) { c := NewConfig(CliTestContext()) assert.Equal(t, ProjectRoot+"/storage/testdata", c.StoragePath()) c.options.StoragePath = "" assert.Equal(t, ProjectRoot+"/storage/testdata/originals/.photoprism/storage", c.StoragePath()) } func TestConfig_TestdataPath(t *testing.T) { c := NewConfig(CliTestContext()) assert.Equal(t, ProjectRoot+"/storage/testdata/testdata", c.TestdataPath()) } func TestConfig_AlbumsPath(t *testing.T) { c := NewConfig(CliTestContext()) // The default albums path has changed from “albums/” to “backup/albums/”. // // If this test fails, please manually move “albums” to the “backup” folder // in the “storage/testdata” directory within your development environment: // https://github.com/photoprism/photoprism/discussions/4520 assert.Equal(t, ProjectRoot+"/storage/testdata/backup/albums", c.BackupAlbumsPath()) } func TestConfig_OriginalsAlbumsPath(t *testing.T) { c := NewConfig(CliTestContext()) assert.Equal(t, ProjectRoot+"/storage/testdata/originals/albums", c.OriginalsAlbumsPath()) } func TestConfig_CreateDirectories(t *testing.T) { t.Run("Success", func(t *testing.T) { testConfigMutex.Lock() defer testConfigMutex.Unlock() c := &Config{ options: NewTestOptions("config"), token: rnd.Base36(8), cache: gc.New(time.Second, time.Minute), } assert.NoError(t, c.CreateDirectories()) }) t.Run("IdenticalPaths", func(t *testing.T) { testConfigMutex.Lock() defer testConfigMutex.Unlock() c := &Config{ options: NewTestOptions("config"), token: rnd.Base36(8), cache: gc.New(time.Second, time.Minute), } c.options.StoragePath = "./testdata" c.options.OriginalsPath = "./testdata" assert.Error(t, c.CreateDirectories()) }) } /* TODO --- FAIL: TestConfig_CreateDirectories2 (0.00s) --- FAIL: TestConfig_CreateDirectories2/asset_path_not_found (0.00s) fs_test.go:142: error expected func TestConfig_CreateDirectories2(t *testing.T) { t.Run("AssetPathNotFound", func(t *testing.T) { testConfigMutex.Lock() defer testConfigMutex.Unlock() c := &Config{ options: NewTestOptions(), token: rnd.Base36(8), } c.options.AssetsPath = "" err := c.CreateDirectories() if err == nil { t.Fatal("error expected") } assert.Contains(t, err.Error(), "assets path not found") c.options.AssetsPath = "/-*&^%$#@!`~" err2 := c.CreateDirectories() if err2 == nil { t.Fatal("error expected") } assert.Contains(t, err2.Error(), "check config and permissions") }) t.Run("StoragePathError", func(t *testing.T) { testConfigMutex.Lock() defer testConfigMutex.Unlock() c := &Config{ options: NewTestOptions(), token: rnd.Base36(8), } c.options.StoragePath = "/-*&^%$#@!`~" err2 := c.CreateDirectories() if err2 == nil { t.Fatal("error expected") } assert.Contains(t, err2.Error(), "check config and permissions") }) t.Run("OriginalsPathNotFound", func(t *testing.T) { testConfigMutex.Lock() defer testConfigMutex.Unlock() c := &Config{ options: NewTestOptions(), token: rnd.Base36(8), } c.options.OriginalsPath = "" err := c.CreateDirectories() if err == nil { t.Fatal("error expected") } assert.Contains(t, err.Error(), "originals path not found") c.options.OriginalsPath = "/-*&^%$#@!`~" err2 := c.CreateDirectories() if err2 == nil { t.Fatal("error expected") } assert.Contains(t, err2.Error(), "check config and permissions") }) t.Run("ImportPathNotFound", func(t *testing.T) { testConfigMutex.Lock() defer testConfigMutex.Unlock() c := &Config{ options: NewTestOptions(), token: rnd.Base36(8), } c.options.ImportPath = "" err := c.CreateDirectories() if err == nil { t.Fatal("error expected") } assert.Contains(t, err.Error(), "import path not found") c.options.ImportPath = "/-*&^%$#@!`~" err2 := c.CreateDirectories() if err2 == nil { t.Fatal("error expected") } assert.Contains(t, err2.Error(), "check config and permissions") }) t.Run("SidecarPathError", func(t *testing.T) { testConfigMutex.Lock() defer testConfigMutex.Unlock() c := &Config{ options: NewTestOptions(), token: rnd.Base36(8), } c.options.SidecarPath = "/-*&^%$#@!`~" err2 := c.CreateDirectories() if err2 == nil { t.Fatal("error expected") } assert.Contains(t, err2.Error(), "check config and permissions") }) t.Run("CachePathError", func(t *testing.T) { testConfigMutex.Lock() defer testConfigMutex.Unlock() c := &Config{ options: NewTestOptions(), token: rnd.Base36(8), } c.options.CachePath = "/-*&^%$#@!`~" err2 := c.CreateDirectories() if err2 == nil { t.Fatal("error expected") } assert.Contains(t, err2.Error(), "check config and permissions") }) t.Run("ConfigPathError", func(t *testing.T) { testConfigMutex.Lock() defer testConfigMutex.Unlock() c := &Config{ options: NewTestOptions(), token: rnd.Base36(8), } c.options.ConfigPath = "/-*&^%$#@!`~" err2 := c.CreateDirectories() if err2 == nil { t.Fatal("error expected") } assert.Contains(t, err2.Error(), "check config and permissions") }) t.Run("TempPathError", func(t *testing.T) { testConfigMutex.Lock() defer testConfigMutex.Unlock() c := &Config{ options: NewTestOptions(), token: rnd.Base36(8), } c.options.TempPath = "/-*&^%$#@!`~" err2 := c.CreateDirectories() if err2 == nil { t.Fatal("error expected") } assert.Contains(t, err2.Error(), "check config and permissions") }) } */ func TestConfig_PIDFilename2(t *testing.T) { c := NewConfig(CliTestContext()) assert.Equal(t, ProjectRoot+"/storage/testdata/photoprism.pid", c.PIDFilename()) c.options.PIDFilename = ProjectRoot + "/internal/config/testdata/test.pid" assert.Equal(t, ProjectRoot+"/internal/config/testdata/test.pid", c.PIDFilename()) } func TestConfig_LogFilename2(t *testing.T) { c := NewConfig(CliTestContext()) assert.Equal(t, ProjectRoot+"/storage/testdata/photoprism.log", c.LogFilename()) c.options.LogFilename = ProjectRoot + "/internal/config/testdata/test.log" assert.Equal(t, ProjectRoot+"/internal/config/testdata/test.log", c.LogFilename()) } func TestConfig_OriginalsPath2(t *testing.T) { c := NewConfig(CliTestContext()) assert.Equal(t, ProjectRoot+"/storage/testdata/originals", c.OriginalsPath()) c.options.OriginalsPath = "" if s := c.OriginalsPath(); s != "" && s != "/photoprism/originals" { t.Errorf("unexpected originals path: %s", s) } } func TestConfig_OriginalsDeletable(t *testing.T) { c := TestConfig() c.Settings().Features.Delete = true c.Options().ReadOnly = false c.AssertTestData(t) assert.True(t, c.OriginalsDeletable()) } func TestConfig_ImportAllow(t *testing.T) { c := NewConfig(CliTestContext()) c.options.ImportAllow = "jpg, PNG,pdf" assert.Equal(t, "jpg, pdf, png", c.ImportAllow().String()) c.options.ImportAllow = "" assert.Len(t, c.ImportAllow(), 0) assert.Equal(t, "", c.ImportAllow().String()) } func TestConfig_AssetsPath(t *testing.T) { c := NewConfig(CliTestContext()) assert.True(t, strings.HasSuffix(c.AssetsPath(), "/assets")) assert.Equal(t, ProjectRoot+"/assets", c.AssetsPath()) c.options.AssetsPath = "" if s := c.AssetsPath(); s != "" && s != "/opt/photoprism/assets" { t.Errorf("unexpected assets path: %s", s) } } func TestConfig_ProfilesPath(t *testing.T) { c := NewConfig(CliTestContext()) result := c.ProfilesPath() assert.True(t, strings.HasSuffix(result, "/assets/profiles")) assert.True(t, fs.PathExists(result)) } func TestConfig_IccProfilesPath(t *testing.T) { c := NewConfig(CliTestContext()) result := c.IccProfilesPath() assert.True(t, strings.HasSuffix(result, "/assets/profiles/icc")) } func TestConfig_CustomAssetsPath(t *testing.T) { c := NewConfig(CliTestContext()) assert.Equal(t, "", c.CustomAssetsPath()) } func TestConfig_MariadbBin(t *testing.T) { c := NewConfig(CliTestContext()) assert.Contains(t, c.MariadbBin(), "mariadb") } func TestConfig_MariadbDumpBin(t *testing.T) { c := NewConfig(CliTestContext()) assert.Contains(t, c.MariadbDumpBin(), "mariadb-dump") } func TestConfig_SqliteBin(t *testing.T) { c := NewConfig(CliTestContext()) assert.Contains(t, c.SqliteBin(), "sqlite") } func TestConfig_SettingsYamlDefaults(t *testing.T) { c := NewConfig(CliTestContext()) name1 := c.SettingsYamlDefaults(c.SettingsYaml()) t.Logf("(1) DefaultsYaml: %s", c.DefaultsYaml()) t.Logf("(1) SettingsYaml: %s", c.SettingsYaml()) t.Logf("(1) SettingsYamlDefaults: %s", name1) assert.Equal(t, c.SettingsYaml(), name1) c.options.ConfigPath = "/tmp/012345678ABC" c.options.DefaultsYaml = "testdata/etc/defaults.yml" name2 := c.SettingsYamlDefaults("") t.Logf("(2) DefaultsYaml: %s", c.DefaultsYaml()) t.Logf("(2) SettingsYaml: %s", c.SettingsYaml()) t.Logf("(2) SettingsYamlDefaults: %s", name2) assert.True(t, strings.HasSuffix(name2, "testdata/etc/settings.yml")) name3 := c.SettingsYamlDefaults(c.SettingsYaml()) t.Logf("(3) DefaultsYaml: %s", c.DefaultsYaml()) t.Logf("(3) SettingsYaml: %s", c.SettingsYaml()) t.Logf("(3) SettingsYamlDefaults: %s", name3) assert.True(t, strings.HasSuffix(name3, "testdata/etc/settings.yml")) assert.NotEqual(t, c.SettingsYaml(), name1) assert.NotEqual(t, c.SettingsYaml(), name3) } func TestDefaultsYamlResolution(t *testing.T) { t.Run("ExplicitFlag", func(t *testing.T) { ctx := CliTestContext() file := filepath.Join(t.TempDir(), "explicit-defaults.yml") require.NoError(t, os.WriteFile(file, []byte("Test: true"), fs.ModeFile)) require.NoError(t, ctx.Set("defaults-yaml", file)) got := defaultsYaml(ctx) require.Equal(t, fs.Abs(file), got) }) t.Run("ConfigFallback", func(t *testing.T) { ctx := CliTestContext() configDir := filepath.Join(t.TempDir(), "cfg") require.NoError(t, os.MkdirAll(configDir, fs.ModeDir)) file := filepath.Join(configDir, "defaults.yml") require.NoError(t, os.WriteFile(file, []byte("SiteUrl: https://example.com"), fs.ModeFile)) require.NoError(t, ctx.Set("defaults-yaml", "")) require.NoError(t, ctx.Set("config-path", configDir)) got := defaultsYaml(ctx) require.Equal(t, fs.Abs(file), got) }) t.Run("MissingReturnsEmpty", func(t *testing.T) { ctx := CliTestContext() require.NoError(t, ctx.Set("defaults-yaml", filepath.Join(t.TempDir(), "missing.yml"))) require.NoError(t, ctx.Set("config-path", t.TempDir())) require.Equal(t, "", defaultsYaml(ctx)) }) }