mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 00:34:13 +01:00
Backend: Introduce optimized test config helpers to improve performance
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
@@ -243,6 +243,7 @@ If anything in this file conflicts with the `Makefile` or the Developer Guide, t
|
||||
- In `internal/photoprism` tests, rely on `photoprism.Config()` for runtime-accurate behavior; only build a new config if you replace it via `photoprism.SetConfig`.
|
||||
- Generate identifiers with `rnd.GenerateUID(entity.ClientUID)` for OAuth client IDs and `rnd.UUIDv7()` for node UUIDs; treat `node.uuid` as required in responses.
|
||||
- Shared fixtures live under `storage/testdata`; `NewTestConfig("<pkg>")` already calls `InitializeTestData()`, but call `c.InitializeTestData()` (and optionally `c.AssertTestData(t)`) when you construct custom configs so originals/import/cache/temp exist. `InitializeTestData()` clears old data, downloads fixtures if needed, then calls `CreateDirectories()`.
|
||||
- For slimmer tests that only need config objects, prefer the new helpers in `internal/config/test.go`: `NewMinimalTestConfig(t.TempDir())` when no database is needed, or `NewMinimalTestConfigWithDb("<pkg>", t.TempDir())` to spin up an isolated SQLite schema without seeding all fixtures.
|
||||
|
||||
### Roles & ACL
|
||||
- Map roles via the shared tables: users through `acl.ParseRole(s)` / `acl.UserRoles[...]`, clients through `acl.ClientRoles[...]`.
|
||||
|
||||
@@ -253,11 +253,12 @@ type portalJWTFixture struct {
|
||||
|
||||
func newPortalJWTFixture(t *testing.T, suffix string) portalJWTFixture {
|
||||
t.Helper()
|
||||
t.Setenv("PHOTOPRISM_STORAGE_PATH", t.TempDir())
|
||||
|
||||
origConf := get.Config()
|
||||
t.Cleanup(func() { get.SetConfig(origConf) })
|
||||
|
||||
nodeConf := config.NewTestConfig("auth-any-portal-jwt-" + suffix)
|
||||
nodeConf := config.NewMinimalTestConfigWithDb("auth-any-portal-jwt-"+suffix, t.TempDir())
|
||||
|
||||
nodeConf.Options().NodeRole = cluster.RoleInstance
|
||||
nodeConf.Options().Public = false
|
||||
clusterUUID := rnd.UUID()
|
||||
@@ -265,7 +266,8 @@ func newPortalJWTFixture(t *testing.T, suffix string) portalJWTFixture {
|
||||
nodeUUID := nodeConf.NodeUUID()
|
||||
nodeConf.Options().PortalUrl = "https://portal.example.test"
|
||||
|
||||
portalConf := config.NewTestConfig("auth-any-portal-jwt-issuer-" + suffix)
|
||||
portalConf := config.NewMinimalTestConfigWithDb("auth-any-portal-jwt-issuer-"+suffix, t.TempDir())
|
||||
|
||||
portalConf.Options().NodeRole = cluster.RolePortal
|
||||
portalConf.Options().ClusterUUID = clusterUUID
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
cfg "github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
)
|
||||
@@ -17,9 +17,6 @@ func TestMain(m *testing.M) {
|
||||
log.SetLevel(logrus.TraceLevel)
|
||||
event.AuditLog = log
|
||||
|
||||
c := config.TestConfig()
|
||||
defer c.CloseDb()
|
||||
|
||||
// Run unit tests.
|
||||
code := m.Run()
|
||||
|
||||
@@ -28,3 +25,7 @@ func TestMain(m *testing.M) {
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func newTestConfig(t *testing.T) *cfg.Config {
|
||||
return cfg.NewMinimalTestConfig(t.TempDir())
|
||||
}
|
||||
|
||||
@@ -9,12 +9,11 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
cfg "github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
)
|
||||
|
||||
func TestManagerEnsureActiveKey(t *testing.T) {
|
||||
c := cfg.TestConfig()
|
||||
c := newTestConfig(t)
|
||||
m, err := NewManager(c)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, m)
|
||||
@@ -53,7 +52,7 @@ func TestManagerEnsureActiveKey(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestManagerGenerateSecondKey(t *testing.T) {
|
||||
c := cfg.TestConfig()
|
||||
c := newTestConfig(t)
|
||||
m, err := NewManager(c)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
||||
@@ -13,12 +13,11 @@ import (
|
||||
|
||||
gojwt "github.com/golang-jwt/jwt/v5"
|
||||
|
||||
cfg "github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
func TestVerifierPrimeAndVerify(t *testing.T) {
|
||||
portalCfg := cfg.TestConfig()
|
||||
portalCfg := newTestConfig(t)
|
||||
clusterUUID := rnd.UUIDv7()
|
||||
portalCfg.Options().ClusterUUID = clusterUUID
|
||||
|
||||
@@ -47,7 +46,7 @@ func TestVerifierPrimeAndVerify(t *testing.T) {
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
nodeCfg := cfg.NewTestConfig("jwt-verifier-node")
|
||||
nodeCfg := newTestConfig(t)
|
||||
nodeCfg.SetJWKSUrl(server.URL + "/.well-known/jwks.json")
|
||||
nodeCfg.Options().ClusterUUID = clusterUUID
|
||||
nodeUUID := nodeCfg.NodeUUID()
|
||||
@@ -105,7 +104,7 @@ func TestVerifierPrimeAndVerify(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestIssuerClampTTL(t *testing.T) {
|
||||
portalCfg := cfg.TestConfig()
|
||||
portalCfg := newTestConfig(t)
|
||||
mgr, err := NewManager(portalCfg)
|
||||
require.NoError(t, err)
|
||||
mgr.now = func() time.Time { return time.Unix(0, 0) }
|
||||
|
||||
@@ -28,7 +28,13 @@ func TestMain(m *testing.M) {
|
||||
log.SetLevel(logrus.TraceLevel)
|
||||
event.AuditLog = log
|
||||
|
||||
c := config.NewTestConfig("commands")
|
||||
tempDir, err := os.MkdirTemp("", "commands-test")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
c := config.NewMinimalTestConfigWithDb("commands", tempDir)
|
||||
get.SetConfig(c)
|
||||
|
||||
// Keep DB connection open for the duration of this package's tests to
|
||||
|
||||
@@ -82,7 +82,7 @@ func TestConfig_ClientShareConfig(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestConfig_ClientUser(t *testing.T) {
|
||||
c := NewTestConfig("config")
|
||||
c := NewMinimalTestConfigWithDb("client-user", t.TempDir())
|
||||
c.SetAuthMode(AuthModePasswd)
|
||||
|
||||
assert.Equal(t, AuthModePasswd, c.AuthMode())
|
||||
@@ -112,7 +112,7 @@ func TestConfig_ClientUser(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestConfig_ClientRoleConfig(t *testing.T) {
|
||||
c := NewTestConfig("config")
|
||||
c := NewMinimalTestConfigWithDb("client-role", t.TempDir())
|
||||
c.SetAuthMode(AuthModePasswd)
|
||||
|
||||
assert.Equal(t, AuthModePasswd, c.AuthMode())
|
||||
|
||||
@@ -45,13 +45,7 @@ func testDataPath(assetsPath string) string {
|
||||
var PkgNameRegexp = regexp.MustCompile("[^a-zA-Z\\-_]+")
|
||||
|
||||
// NewTestOptions returns valid config options for tests.
|
||||
func NewTestOptions(pkg string) *Options {
|
||||
// Find assets path.
|
||||
assetsPath := os.Getenv("PHOTOPRISM_ASSETS_PATH")
|
||||
if assetsPath == "" {
|
||||
fs.Abs("../../assets")
|
||||
}
|
||||
|
||||
func NewTestOptions(dbName string) *Options {
|
||||
// Find storage path.
|
||||
storagePath := os.Getenv("PHOTOPRISM_STORAGE_PATH")
|
||||
if storagePath == "" {
|
||||
@@ -60,7 +54,43 @@ func NewTestOptions(pkg string) *Options {
|
||||
|
||||
dataPath := filepath.Join(storagePath, fs.TestdataDir)
|
||||
|
||||
pkg = PkgNameRegexp.ReplaceAllString(pkg, "")
|
||||
return NewTestOptionsForPath(dbName, dataPath)
|
||||
}
|
||||
|
||||
// NewTestOptionsForPath returns new test Options using the specified data path as storage.
|
||||
func NewTestOptionsForPath(dbName, dataPath string) *Options {
|
||||
// Default to storage/testdata is no path was specified.
|
||||
if dataPath == "" {
|
||||
storagePath := os.Getenv("PHOTOPRISM_STORAGE_PATH")
|
||||
|
||||
if storagePath == "" {
|
||||
storagePath = fs.Abs("../../storage")
|
||||
}
|
||||
|
||||
dataPath = filepath.Join(storagePath, fs.TestdataDir)
|
||||
}
|
||||
|
||||
dataPath = fs.Abs(dataPath)
|
||||
|
||||
if err := fs.MkdirAll(dataPath); err != nil {
|
||||
log.Errorf("config: %s (create test data path)", err)
|
||||
return &Options{}
|
||||
}
|
||||
|
||||
configPath := filepath.Join(dataPath, "config")
|
||||
|
||||
if err := fs.MkdirAll(configPath); err != nil {
|
||||
log.Errorf("config: %s (create test config path)", err)
|
||||
return &Options{}
|
||||
}
|
||||
|
||||
// Find assets path.
|
||||
assetsPath := os.Getenv("PHOTOPRISM_ASSETS_PATH")
|
||||
if assetsPath == "" {
|
||||
fs.Abs("../../assets")
|
||||
}
|
||||
|
||||
dbName = PkgNameRegexp.ReplaceAllString(dbName, "")
|
||||
driver := os.Getenv("PHOTOPRISM_TEST_DRIVER")
|
||||
dsn := os.Getenv("PHOTOPRISM_TEST_DSN")
|
||||
|
||||
@@ -75,16 +105,16 @@ func NewTestOptions(pkg string) *Options {
|
||||
|
||||
// Set default database DSN.
|
||||
if driver == SQLite3 {
|
||||
if dsn == "" && pkg != "" {
|
||||
if dsn = fmt.Sprintf(".%s.db", clean.TypeLower(pkg)); !fs.FileExists(dsn) {
|
||||
log.Debugf("sqlite: test database %s does not already exist", clean.Log(dsn))
|
||||
if dsn == "" && dbName != "" {
|
||||
if dsn = fmt.Sprintf(".%s.db", clean.TypeLower(dbName)); !fs.FileExists(dsn) {
|
||||
log.Tracef("sqlite: test database %s does not already exist", clean.Log(dsn))
|
||||
} else if err := os.Remove(dsn); err != nil {
|
||||
log.Errorf("sqlite: failed to remove existing test database %s (%s)", clean.Log(dsn), err)
|
||||
}
|
||||
} else if dsn == "" || dsn == SQLiteTestDB {
|
||||
dsn = SQLiteTestDB
|
||||
if !fs.FileExists(dsn) {
|
||||
log.Debugf("sqlite: test database %s does not already exist", clean.Log(dsn))
|
||||
log.Tracef("sqlite: test database %s does not already exist", clean.Log(dsn))
|
||||
} else if err := os.Remove(dsn); err != nil {
|
||||
log.Errorf("sqlite: failed to remove existing test database %s (%s)", clean.Log(dsn), err)
|
||||
}
|
||||
@@ -92,7 +122,7 @@ func NewTestOptions(pkg string) *Options {
|
||||
}
|
||||
|
||||
// Test config options.
|
||||
c := &Options{
|
||||
opts := &Options{
|
||||
Name: "PhotoPrism",
|
||||
Version: "0.0.0",
|
||||
Copyright: "(c) 2018-2025 PhotoPrism UG. All rights reserved.",
|
||||
@@ -111,12 +141,14 @@ func NewTestOptions(pkg string) *Options {
|
||||
IndexSchedule: DefaultIndexSchedule,
|
||||
AutoImport: 7200,
|
||||
StoragePath: dataPath,
|
||||
CachePath: dataPath + "/cache",
|
||||
OriginalsPath: dataPath + "/originals",
|
||||
ImportPath: dataPath + "/import",
|
||||
ConfigPath: dataPath + "/config",
|
||||
SidecarPath: dataPath + "/sidecar",
|
||||
TempPath: dataPath + "/temp",
|
||||
CachePath: filepath.Join(dataPath, "cache"),
|
||||
OriginalsPath: filepath.Join(dataPath, "originals"),
|
||||
ImportPath: filepath.Join(dataPath, "import"),
|
||||
ConfigPath: configPath,
|
||||
DefaultsYaml: filepath.Join(configPath, "defaults.yml"),
|
||||
OptionsYaml: filepath.Join(configPath, "options.yml"),
|
||||
SidecarPath: filepath.Join(dataPath, "sidecar"),
|
||||
TempPath: filepath.Join(dataPath, "temp"),
|
||||
BackupRetain: DefaultBackupRetain,
|
||||
BackupSchedule: DefaultBackupSchedule,
|
||||
DatabaseDriver: driver,
|
||||
@@ -128,7 +160,7 @@ func NewTestOptions(pkg string) *Options {
|
||||
DetectNSFW: true,
|
||||
}
|
||||
|
||||
return c
|
||||
return opts
|
||||
}
|
||||
|
||||
// NewTestOptionsError returns invalid config options for tests.
|
||||
@@ -162,11 +194,94 @@ func TestConfig() *Config {
|
||||
return testConfig
|
||||
}
|
||||
|
||||
// NewTestConfig returns a valid test config.
|
||||
// NewMinimalTestConfig creates a lightweight test Config (no DB, minimal filesystem).
|
||||
//
|
||||
// Not suitable for tests requiring a database or pre-created storage directories.
|
||||
func NewMinimalTestConfig(dataPath string) *Config {
|
||||
return NewIsolatedTestConfig("", dataPath, false)
|
||||
}
|
||||
|
||||
var testDbCache []byte
|
||||
var testDbMutex sync.Mutex
|
||||
|
||||
// NewMinimalTestConfigWithDb creates a lightweight test Config (minimal filesystem).
|
||||
//
|
||||
// Creates an isolated SQLite DB (cached after first run) without seeding media fixtures.
|
||||
func NewMinimalTestConfigWithDb(dbName, dataPath string) *Config {
|
||||
c := NewIsolatedTestConfig(dbName, dataPath, true)
|
||||
|
||||
cachedDb := false
|
||||
|
||||
// Try to restore test db from cache.
|
||||
if len(testDbCache) > 0 && c.DatabaseDriver() == SQLite3 && !fs.FileExists(c.DatabaseDSN()) {
|
||||
if err := os.WriteFile(c.DatabaseDSN(), testDbCache, fs.ModeFile); err != nil {
|
||||
log.Warnf("config: %s (restore test database)", err)
|
||||
} else {
|
||||
cachedDb = true
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.Init(); err != nil {
|
||||
log.Fatalf("config: %s (init)", err.Error())
|
||||
}
|
||||
|
||||
c.RegisterDb()
|
||||
|
||||
if cachedDb {
|
||||
return c
|
||||
}
|
||||
|
||||
c.InitTestDb()
|
||||
|
||||
if testDbCache == nil && c.DatabaseDriver() == SQLite3 && fs.FileExistsNotEmpty(c.DatabaseDSN()) {
|
||||
testDbMutex.Lock()
|
||||
defer testDbMutex.Unlock()
|
||||
|
||||
if testDbCache != nil {
|
||||
return c
|
||||
}
|
||||
|
||||
if testDb, readErr := os.ReadFile(c.DatabaseDSN()); readErr != nil {
|
||||
log.Warnf("config: could not cache test database (%s)", readErr)
|
||||
} else {
|
||||
testDbCache = testDb
|
||||
}
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// NewIsolatedTestConfig constructs a lightweight Config backed by the provided config path.
|
||||
//
|
||||
// It avoids running migrations or loading test fixtures, making it useful for unit tests that
|
||||
// only need basic access to config options (for example, JWT helpers). The caller should provide
|
||||
// an isolated directory (e.g. via testing.T.TempDir) so temporary files are cleaned up automatically.
|
||||
func NewIsolatedTestConfig(dbName, dataPath string, createDirs bool) *Config {
|
||||
if dataPath == "" {
|
||||
dataPath = filepath.Join(os.TempDir(), "photoprism-test-"+rnd.Base36(6))
|
||||
}
|
||||
|
||||
opts := NewTestOptionsForPath(dbName, dataPath)
|
||||
|
||||
c := &Config{
|
||||
options: opts,
|
||||
token: rnd.Base36(8),
|
||||
}
|
||||
|
||||
if !createDirs {
|
||||
return c
|
||||
}
|
||||
|
||||
if err := c.CreateDirectories(); err != nil {
|
||||
log.Errorf("config: %s (create test directories)", err)
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// NewTestConfig initializes test data so required directories exist before tests run.
|
||||
// See AGENTS.md (Test Data & Fixtures) and specs/dev/backend-testing.md for guidance.
|
||||
func NewTestConfig(pkg string) *Config {
|
||||
func NewTestConfig(dbName string) *Config {
|
||||
defer log.Debug(capture.Time(time.Now(), "config: new test config created"))
|
||||
|
||||
testConfigMutex.Lock()
|
||||
@@ -174,7 +289,7 @@ func NewTestConfig(pkg string) *Config {
|
||||
|
||||
c := &Config{
|
||||
cliCtx: CliTestContext(),
|
||||
options: NewTestOptions(pkg),
|
||||
options: NewTestOptions(dbName),
|
||||
token: rnd.Base36(8),
|
||||
}
|
||||
|
||||
|
||||
@@ -5,14 +5,24 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
c := config.NewTestConfig("service")
|
||||
tempDir, err := os.MkdirTemp("", "internal-photoprism-get")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
c := config.NewMinimalTestConfigWithDb("test", tempDir)
|
||||
|
||||
SetConfig(c)
|
||||
defer c.CloseDb()
|
||||
|
||||
code := m.Run()
|
||||
|
||||
// Remove temporary SQLite files after running the tests.
|
||||
fs.PurgeTestDbFiles(".", false)
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ func TestWebDAVFileName_PathTraversalRejected(t *testing.T) {
|
||||
insideFile := filepath.Join(dir, "ok.txt")
|
||||
assert.NoError(t, fs.WriteString(insideFile, "ok"))
|
||||
|
||||
conf := config.NewTestConfig("server-webdav")
|
||||
conf := newWebDAVTestConfig(t)
|
||||
conf.Options().OriginalsPath = dir
|
||||
|
||||
r := gin.New()
|
||||
@@ -55,7 +55,7 @@ func TestWebDAVFileName_PathTraversalRejected(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestWebDAVFileName_MethodNotPut(t *testing.T) {
|
||||
conf := config.NewTestConfig("server-webdav")
|
||||
conf := newWebDAVTestConfig(t)
|
||||
r := gin.New()
|
||||
grp := r.Group(conf.BaseUri(WebDAVOriginals))
|
||||
req := &http.Request{Method: http.MethodGet}
|
||||
@@ -65,7 +65,7 @@ func TestWebDAVFileName_MethodNotPut(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestWebDAVFileName_ImportBasePath(t *testing.T) {
|
||||
conf := config.NewTestConfig("server-webdav")
|
||||
conf := newWebDAVTestConfig(t)
|
||||
r := gin.New()
|
||||
grp := r.Group(conf.BaseUri(WebDAVImport))
|
||||
// create a real file under import
|
||||
@@ -88,3 +88,7 @@ func TestWebDAVSetFileMtime_FutureIgnored(t *testing.T) {
|
||||
after, _ := os.Stat(file)
|
||||
assert.Equal(t, before.ModTime().Unix(), after.ModTime().Unix())
|
||||
}
|
||||
|
||||
func newWebDAVTestConfig(t *testing.T) *config.Config {
|
||||
return config.NewMinimalTestConfig(t.TempDir())
|
||||
}
|
||||
|
||||
@@ -19,15 +19,15 @@ import (
|
||||
)
|
||||
|
||||
func TestInitConfig_NoPortal_NoOp(t *testing.T) {
|
||||
t.Setenv("PHOTOPRISM_STORAGE_PATH", t.TempDir())
|
||||
c := config.NewTestConfig("bootstrap-np")
|
||||
c := config.NewMinimalTestConfigWithDb("bootstrap", t.TempDir())
|
||||
defer c.CloseDb()
|
||||
|
||||
// Default NodeRole() resolves to instance; no Portal configured.
|
||||
assert.Equal(t, cluster.RoleInstance, c.NodeRole())
|
||||
assert.NoError(t, InitConfig(c))
|
||||
}
|
||||
|
||||
func TestRegister_PersistSecretAndDB(t *testing.T) {
|
||||
t.Setenv("PHOTOPRISM_STORAGE_PATH", t.TempDir())
|
||||
// Fake Portal server.
|
||||
var jwksURL string
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -62,7 +62,9 @@ func TestRegister_PersistSecretAndDB(t *testing.T) {
|
||||
jwksURL = srv.URL + "/.well-known/jwks.json"
|
||||
defer srv.Close()
|
||||
|
||||
c := config.NewTestConfig("bootstrap-reg")
|
||||
c := config.NewMinimalTestConfigWithDb("bootstrap-reg", t.TempDir())
|
||||
defer c.CloseDb()
|
||||
|
||||
// Configure Portal.
|
||||
c.Options().PortalUrl = srv.URL
|
||||
c.Options().JoinToken = "t0k3n"
|
||||
@@ -85,7 +87,6 @@ func TestRegister_PersistSecretAndDB(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestThemeInstall_Missing(t *testing.T) {
|
||||
t.Setenv("PHOTOPRISM_STORAGE_PATH", t.TempDir())
|
||||
// Build a tiny zip in-memory with one file style.css
|
||||
var buf bytes.Buffer
|
||||
zw := zip.NewWriter(&buf)
|
||||
@@ -115,7 +116,9 @@ func TestThemeInstall_Missing(t *testing.T) {
|
||||
jwksURL2 = srv.URL + "/.well-known/jwks.json"
|
||||
defer srv.Close()
|
||||
|
||||
c := config.NewTestConfig("bootstrap-theme")
|
||||
c := config.NewMinimalTestConfigWithDb("bootstrap-theme", t.TempDir())
|
||||
defer c.CloseDb()
|
||||
|
||||
// Point Portal.
|
||||
c.Options().PortalUrl = srv.URL
|
||||
c.Options().JoinToken = "t0k3n"
|
||||
@@ -137,7 +140,6 @@ func TestThemeInstall_Missing(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRegister_SQLite_NoDBPersist(t *testing.T) {
|
||||
t.Setenv("PHOTOPRISM_STORAGE_PATH", t.TempDir())
|
||||
// Portal responds with DB DSN, but local driver is SQLite → must not persist DB.
|
||||
var jwksURL3 string
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -159,7 +161,9 @@ func TestRegister_SQLite_NoDBPersist(t *testing.T) {
|
||||
jwksURL3 = srv.URL + "/.well-known/jwks.json"
|
||||
defer srv.Close()
|
||||
|
||||
c := config.NewTestConfig("bootstrap-sqlite")
|
||||
c := config.NewMinimalTestConfigWithDb("bootstrap-sqlite", t.TempDir())
|
||||
defer c.CloseDb()
|
||||
|
||||
// SQLite driver by default; set Portal.
|
||||
c.Options().PortalUrl = srv.URL
|
||||
c.Options().JoinToken = "t0k3n"
|
||||
@@ -178,7 +182,6 @@ func TestRegister_SQLite_NoDBPersist(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRegister_404_NoRetry(t *testing.T) {
|
||||
t.Setenv("PHOTOPRISM_STORAGE_PATH", t.TempDir())
|
||||
var hits int
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/api/v1/cluster/nodes/register" {
|
||||
@@ -190,7 +193,9 @@ func TestRegister_404_NoRetry(t *testing.T) {
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := config.NewTestConfig("bootstrap-404")
|
||||
c := config.NewMinimalTestConfigWithDb("bootstrap", t.TempDir())
|
||||
defer c.CloseDb()
|
||||
|
||||
c.Options().PortalUrl = srv.URL
|
||||
c.Options().JoinToken = "t0k3n"
|
||||
|
||||
@@ -201,7 +206,6 @@ func TestRegister_404_NoRetry(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestThemeInstall_SkipWhenAppJsExists(t *testing.T) {
|
||||
t.Setenv("PHOTOPRISM_STORAGE_PATH", t.TempDir())
|
||||
// Portal returns a valid zip, but theme dir already has app.js → skip.
|
||||
var served int
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -218,7 +222,9 @@ func TestThemeInstall_SkipWhenAppJsExists(t *testing.T) {
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := config.NewTestConfig("bootstrap-theme-skip")
|
||||
c := config.NewMinimalTestConfigWithDb("bootstrap", t.TempDir())
|
||||
defer c.CloseDb()
|
||||
|
||||
c.Options().PortalUrl = srv.URL
|
||||
c.Options().JoinToken = "t0k3n"
|
||||
|
||||
|
||||
@@ -14,9 +14,8 @@ import (
|
||||
|
||||
// Duplicate names: FindByName should return the most recently updated.
|
||||
func TestClientRegistry_DuplicateNamePrefersLatest(t *testing.T) {
|
||||
c := cfg.NewTestConfig("cluster-registry-dupes")
|
||||
c := cfg.NewMinimalTestConfigWithDb("cluster-registry-dupes", t.TempDir())
|
||||
defer c.CloseDb()
|
||||
assert.NoError(t, c.Init())
|
||||
|
||||
// Create two clients directly to simulate duplicates with same name.
|
||||
c1 := entity.NewClient().SetName("pp-dupe").SetRole("instance")
|
||||
@@ -40,9 +39,8 @@ func TestClientRegistry_DuplicateNamePrefersLatest(t *testing.T) {
|
||||
|
||||
// Role change path: Put should update ClientRole via mapping.
|
||||
func TestClientRegistry_RoleChange(t *testing.T) {
|
||||
c := cfg.NewTestConfig("cluster-registry-role")
|
||||
c := cfg.NewMinimalTestConfigWithDb("cluster-registry-role", t.TempDir())
|
||||
defer c.CloseDb()
|
||||
assert.NoError(t, c.Init())
|
||||
|
||||
r, _ := NewClientRegistryWithConfig(c)
|
||||
n := &Node{Node: cluster.Node{Name: "pp-role", Role: "service"}}
|
||||
|
||||
@@ -13,11 +13,8 @@ import (
|
||||
)
|
||||
|
||||
func TestClientRegistry_PutFindListRotate(t *testing.T) {
|
||||
c := cfg.NewTestConfig("cluster-registry-client")
|
||||
c := cfg.NewMinimalTestConfigWithDb("cluster-registry-client", t.TempDir())
|
||||
defer c.CloseDb()
|
||||
if err := c.Init(); err != nil {
|
||||
t.Fatalf("init config: %v", err)
|
||||
}
|
||||
|
||||
r, err := NewClientRegistryWithConfig(c)
|
||||
assert.NoError(t, err)
|
||||
|
||||
@@ -15,9 +15,8 @@ import (
|
||||
// rule prevents hijacking: the update applies to the UUID's row and does not move
|
||||
// the ClientID from its original node.
|
||||
func TestClientRegistry_ClientIDReuse_CannotHijackExistingUUID(t *testing.T) {
|
||||
c := cfg.NewTestConfig("cluster-registry-cid-hijack")
|
||||
c := cfg.NewMinimalTestConfigWithDb("cluster-registry-cid-hijack", t.TempDir())
|
||||
defer c.CloseDb()
|
||||
assert.NoError(t, c.Init())
|
||||
|
||||
r, _ := NewClientRegistryWithConfig(c)
|
||||
// Seed two independent nodes
|
||||
@@ -51,9 +50,8 @@ func TestClientRegistry_ClientIDReuse_CannotHijackExistingUUID(t *testing.T) {
|
||||
// migrates the row to the new UUID. This mirrors restore flows where a node's ClientID
|
||||
// is reused for a regenerated or reassigned UUID.
|
||||
func TestClientRegistry_ClientIDReuse_ChangesUUIDWhenTargetMissing(t *testing.T) {
|
||||
c := cfg.NewTestConfig("cluster-registry-cid-move")
|
||||
c := cfg.NewMinimalTestConfigWithDb("cluster-registry-cid-move", t.TempDir())
|
||||
defer c.CloseDb()
|
||||
assert.NoError(t, c.Init())
|
||||
|
||||
r, _ := NewClientRegistryWithConfig(c)
|
||||
// Seed one node
|
||||
|
||||
@@ -14,9 +14,8 @@ import (
|
||||
|
||||
// Basic FindByClientID flow with Put and DTO mapping.
|
||||
func TestClientRegistry_FindByClientID(t *testing.T) {
|
||||
c := cfg.NewTestConfig("cluster-registry-find-clientid")
|
||||
c := cfg.NewMinimalTestConfigWithDb("cluster-registry-find-clientid", t.TempDir())
|
||||
defer c.CloseDb()
|
||||
assert.NoError(t, c.Init())
|
||||
|
||||
r, _ := NewClientRegistryWithConfig(c)
|
||||
n := &Node{Node: cluster.Node{Name: "pp-find-client", Role: "instance", UUID: rnd.UUIDv7()}}
|
||||
@@ -34,9 +33,8 @@ func TestClientRegistry_FindByClientID(t *testing.T) {
|
||||
|
||||
// Simulate client ID changing after a restore: old row removed, new row created with same NodeUUID.
|
||||
func TestClientRegistry_ClientIDChangedAfterRestore(t *testing.T) {
|
||||
c := cfg.NewTestConfig("cluster-registry-clientid-restore")
|
||||
c := cfg.NewMinimalTestConfigWithDb("cluster-registry-clientid-restore", t.TempDir())
|
||||
defer c.CloseDb()
|
||||
assert.NoError(t, c.Init())
|
||||
|
||||
uuid := rnd.UUIDv7()
|
||||
// Original row
|
||||
@@ -71,9 +69,8 @@ func TestClientRegistry_ClientIDChangedAfterRestore(t *testing.T) {
|
||||
|
||||
// Names swapped between two nodes: UUIDs must remain authoritative.
|
||||
func TestClientRegistry_SwapNames_UUIDAuthoritative(t *testing.T) {
|
||||
c := cfg.NewTestConfig("cluster-registry-swap-names")
|
||||
c := cfg.NewMinimalTestConfigWithDb("cluster-registry-swap-names", t.TempDir())
|
||||
defer c.CloseDb()
|
||||
assert.NoError(t, c.Init())
|
||||
|
||||
r, _ := NewClientRegistryWithConfig(c)
|
||||
a := &Node{Node: cluster.Node{UUID: rnd.UUIDv7(), Name: "pp-a", Role: "instance"}}
|
||||
@@ -117,9 +114,8 @@ func TestClientRegistry_SwapNames_UUIDAuthoritative(t *testing.T) {
|
||||
|
||||
// Ensure DB driver and fields round-trip through Put → toNode → BuildClusterNode.
|
||||
func TestClientRegistry_DBDriverAndFields(t *testing.T) {
|
||||
c := cfg.NewTestConfig("cluster-registry-dbdriver")
|
||||
c := cfg.NewMinimalTestConfigWithDb("cluster-registry-dbdriver", t.TempDir())
|
||||
defer c.CloseDb()
|
||||
assert.NoError(t, c.Init())
|
||||
|
||||
r, _ := NewClientRegistryWithConfig(c)
|
||||
n := &Node{Node: cluster.Node{UUID: rnd.UUIDv7(), Name: "pp-db", Role: "instance"}}
|
||||
|
||||
@@ -12,9 +12,8 @@ import (
|
||||
|
||||
// Ensure List() excludes clients that look like nodes by role but have no NodeUUID.
|
||||
func TestClientRegistry_ListExcludesNodeRoleWithoutUUID(t *testing.T) {
|
||||
c := cfg.NewTestConfig("cluster-registry-list-exclude-node-role")
|
||||
c := cfg.NewMinimalTestConfigWithDb("cluster-registry-list-exclude-node-role", t.TempDir())
|
||||
defer c.CloseDb()
|
||||
assert.NoError(t, c.Init())
|
||||
|
||||
// Bad records: node-like roles but empty NodeUUID
|
||||
bad1 := entity.NewClient().SetName("pp-bad1").SetRole("instance")
|
||||
|
||||
@@ -14,9 +14,8 @@ import (
|
||||
|
||||
// Rotating secret selects the latest row for a UUID and persists rotation timestamp and password.
|
||||
func TestClientRegistry_RotateSecretByUUID_LatestRow(t *testing.T) {
|
||||
c := cfg.NewTestConfig("cluster-registry-rotate-latest")
|
||||
c := cfg.NewMinimalTestConfigWithDb("cluster-registry-rotate-latest", t.TempDir())
|
||||
defer c.CloseDb()
|
||||
assert.NoError(t, c.Init())
|
||||
|
||||
r, _ := NewClientRegistryWithConfig(c)
|
||||
uuid := rnd.UUIDv7()
|
||||
|
||||
@@ -26,9 +26,8 @@ func TestMain(m *testing.M) {
|
||||
}
|
||||
|
||||
func TestClientRegistry_GetAndDelete(t *testing.T) {
|
||||
c := cfg.NewTestConfig("cluster-registry-delete")
|
||||
c := cfg.NewMinimalTestConfigWithDb("cluster-registry-delete", t.TempDir())
|
||||
defer c.CloseDb()
|
||||
assert.NoError(t, c.Init())
|
||||
|
||||
r, _ := NewClientRegistryWithConfig(c)
|
||||
|
||||
@@ -68,9 +67,8 @@ func TestClientRegistry_GetAndDelete(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestClientRegistry_ListOrderByUpdatedAtDesc(t *testing.T) {
|
||||
c := cfg.NewTestConfig("cluster-registry-order")
|
||||
c := cfg.NewMinimalTestConfigWithDb("cluster-registry-order", t.TempDir())
|
||||
defer c.CloseDb()
|
||||
assert.NoError(t, c.Init())
|
||||
|
||||
r, _ := NewClientRegistryWithConfig(c)
|
||||
|
||||
@@ -160,9 +158,8 @@ func TestNodeOptsForSession_AdminVsNonAdmin(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestToNode_Mapping(t *testing.T) {
|
||||
c := cfg.NewTestConfig("cluster-registry-map")
|
||||
c := cfg.NewMinimalTestConfigWithDb("cluster-registry-map", t.TempDir())
|
||||
defer c.CloseDb()
|
||||
assert.NoError(t, c.Init())
|
||||
|
||||
m := entity.NewClient().SetName("pp-map").SetRole("instance")
|
||||
m.NodeUUID = rnd.UUIDv7()
|
||||
@@ -191,7 +188,7 @@ func TestToNode_Mapping(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestClientRegistry_GetClusterNodeByUUID(t *testing.T) {
|
||||
c := cfg.NewTestConfig("cluster-registry-getbyuuid")
|
||||
c := cfg.NewMinimalTestConfigWithDb("cluster-registry-getbyuuid", t.TempDir())
|
||||
defer c.CloseDb()
|
||||
assert.NoError(t, c.Init())
|
||||
|
||||
@@ -210,7 +207,7 @@ func TestClientRegistry_GetClusterNodeByUUID(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestClientRegistry_FindByName_NormalizesDNSLabel(t *testing.T) {
|
||||
c := cfg.NewTestConfig("cluster-registry-findname")
|
||||
c := cfg.NewMinimalTestConfigWithDb("cluster-registry-findname", t.TempDir())
|
||||
defer c.CloseDb()
|
||||
assert.NoError(t, c.Init())
|
||||
|
||||
|
||||
@@ -14,9 +14,8 @@ import (
|
||||
|
||||
// UUID-first upsert: Put finds existing row by UUID and updates fields.
|
||||
func TestClientRegistry_PutUpdateByUUID(t *testing.T) {
|
||||
c := cfg.NewTestConfig("cluster-registry-put-uuid")
|
||||
c := cfg.NewMinimalTestConfigWithDb("cluster-registry-put-uuid", t.TempDir())
|
||||
defer c.CloseDb()
|
||||
assert.NoError(t, c.Init())
|
||||
|
||||
r, _ := NewClientRegistryWithConfig(c)
|
||||
uuid := rnd.UUIDv7()
|
||||
@@ -47,9 +46,8 @@ func TestClientRegistry_PutUpdateByUUID(t *testing.T) {
|
||||
|
||||
// Latest-by-UpdatedAt when multiple rows share the same NodeUUID (historical duplicates).
|
||||
func TestClientRegistry_FindByNodeUUID_PrefersLatest(t *testing.T) {
|
||||
c := cfg.NewTestConfig("cluster-registry-find-uuid-latest")
|
||||
c := cfg.NewMinimalTestConfigWithDb("cluster-registry-find-uuid-latest", t.TempDir())
|
||||
defer c.CloseDb()
|
||||
assert.NoError(t, c.Init())
|
||||
|
||||
uuid := rnd.UUIDv7()
|
||||
// Create two raw client rows with the same NodeUUID and different UpdatedAt
|
||||
@@ -74,9 +72,8 @@ func TestClientRegistry_FindByNodeUUID_PrefersLatest(t *testing.T) {
|
||||
|
||||
// DeleteAllByUUID removes all rows that share a NodeUUID.
|
||||
func TestClientRegistry_DeleteAllByUUID(t *testing.T) {
|
||||
c := cfg.NewTestConfig("cluster-registry-delete-all")
|
||||
c := cfg.NewMinimalTestConfigWithDb("cluster-registry-delete-all", t.TempDir())
|
||||
defer c.CloseDb()
|
||||
assert.NoError(t, c.Init())
|
||||
|
||||
uuid := rnd.UUIDv7()
|
||||
// Two rows with same UUID
|
||||
@@ -99,9 +96,8 @@ func TestClientRegistry_DeleteAllByUUID(t *testing.T) {
|
||||
|
||||
// List() should only include clients that represent cluster nodes (i.e., have a NodeUUID).
|
||||
func TestClientRegistry_ListOnlyUUID(t *testing.T) {
|
||||
c := cfg.NewTestConfig("cluster-registry-list-only-uuid")
|
||||
c := cfg.NewMinimalTestConfigWithDb("cluster-registry-list-only-uuid", t.TempDir())
|
||||
defer c.CloseDb()
|
||||
assert.NoError(t, c.Init())
|
||||
|
||||
// Create one client with empty NodeUUID (non-node), and one proper node
|
||||
nonNode := entity.NewClient().SetName("webapp").SetRole("client")
|
||||
@@ -122,9 +118,8 @@ func TestClientRegistry_ListOnlyUUID(t *testing.T) {
|
||||
|
||||
// Put should prefer UUID over ClientID when both are provided, avoiding cross-attachment.
|
||||
func TestClientRegistry_PutPrefersUUIDOverClientID(t *testing.T) {
|
||||
c := cfg.NewTestConfig("cluster-registry-put-prefers-uuid")
|
||||
c := cfg.NewMinimalTestConfigWithDb("cluster-registry-put-prefers-uuid", t.TempDir())
|
||||
defer c.CloseDb()
|
||||
assert.NoError(t, c.Init())
|
||||
|
||||
r, _ := NewClientRegistryWithConfig(c)
|
||||
// Seed two separate records
|
||||
|
||||
@@ -15,7 +15,13 @@ func TestMain(m *testing.M) {
|
||||
log = logrus.StandardLogger()
|
||||
log.SetLevel(logrus.TraceLevel)
|
||||
|
||||
c := config.NewTestConfig("avatar")
|
||||
tempDir, err := os.MkdirTemp("", "avatar-test")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
c := config.NewMinimalTestConfigWithDb("avatar", tempDir)
|
||||
get.SetConfig(c)
|
||||
photoprism.SetConfig(c)
|
||||
defer c.CloseDb()
|
||||
|
||||
Reference in New Issue
Block a user