Backend: Introduce optimized test config helpers to improve performance

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer
2025-09-25 23:09:52 +02:00
parent ebb0410b20
commit 660c0a89db
20 changed files with 227 additions and 99 deletions

View File

@@ -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[...]`.

View File

@@ -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

View File

@@ -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())
}

View File

@@ -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)

View File

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

View File

@@ -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

View File

@@ -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())

View File

@@ -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),
}

View File

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

View File

@@ -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())
}

View File

@@ -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"

View File

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

View File

@@ -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)

View File

@@ -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

View File

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

View File

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

View File

@@ -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()

View File

@@ -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())

View File

@@ -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

View File

@@ -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()