Files
photoprism/internal/commands/cluster_test.go
2025-09-19 04:15:53 +02:00

151 lines
5.1 KiB
Go

package commands
// NOTE: A number of non-cluster CLI commands defer conf.Shutdown(), which
// closes the shared DB connection for the process. In the commands test
// harness we reopen the DB before each run, but tests that do direct
// registry/DB access (without going through a CLI action) can still observe
// a closed connection if another test has just called Shutdown().
//
// TODO: Investigate centralizing DB lifecycle for commands tests (e.g.,
// a package-level test harness that prevents Shutdown from closing the DB,
// or injecting a mock Shutdown) so these tests don't need re-registration
// or special handling. See also commands_test.go RunWithTestContext.
import (
"archive/zip"
"bytes"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/photoprism/photoprism/internal/photoprism/get"
reg "github.com/photoprism/photoprism/internal/service/cluster/registry"
"github.com/photoprism/photoprism/pkg/fs"
)
func TestClusterSummaryCommand(t *testing.T) {
t.Run("NotPortal", func(t *testing.T) {
out, err := RunWithTestContext(ClusterSummaryCommand, []string{"summary"})
assert.Error(t, err)
_ = out
})
}
func TestClusterNodesListCommand(t *testing.T) {
t.Run("NotPortal", func(t *testing.T) {
out, err := RunWithTestContext(ClusterNodesListCommand, []string{"ls"})
assert.Error(t, err)
_ = out
})
}
func TestClusterNodesShowCommand(t *testing.T) {
t.Run("NotFound", func(t *testing.T) {
_ = os.Setenv("PHOTOPRISM_NODE_ROLE", "portal")
defer os.Unsetenv("PHOTOPRISM_NODE_ROLE")
out, err := RunWithTestContext(ClusterNodesShowCommand, []string{"show", "does-not-exist"})
assert.Error(t, err)
_ = out
})
}
func TestClusterThemePullCommand(t *testing.T) {
t.Run("NotPortal", func(t *testing.T) {
out, err := RunWithTestContext(ClusterThemePullCommand.Subcommands[0], []string{"pull"})
assert.Error(t, err)
_ = out
})
}
func TestClusterRegisterCommand(t *testing.T) {
t.Run("ValidationMissingURL", func(t *testing.T) {
out, err := RunWithTestContext(ClusterRegisterCommand, []string{"register", "--name", "pp-node-01", "--role", "instance", "--join-token", "token"})
assert.Error(t, err)
_ = out
})
}
func TestClusterSuccessPaths_PortalLocal(t *testing.T) {
// TODO: This integration-style test performs direct registry writes and
// multiple CLI actions. Other commands in this package may call Shutdown()
// under test, closing the DB unexpectedly and causing flakiness.
// Skipping for now; the cluster API/registry unit tests cover the logic.
t.Skip("todo: tests may close database connection, refactoring needed")
// Enable portal mode for local admin commands.
c := get.Config()
c.Options().NodeRole = "portal"
// Some commands in previous tests may have closed the DB; ensure it's registered.
c.RegisterDb()
// Ensure registry and theme paths exist.
portCfg := c.PortalConfigPath()
nodesDir := filepath.Join(portCfg, "nodes")
themeDir := filepath.Join(portCfg, "theme")
assert.NoError(t, fs.MkdirAll(nodesDir))
assert.NoError(t, fs.MkdirAll(themeDir))
// Create a theme file to zip.
themeFile := filepath.Join(themeDir, "test.txt")
assert.NoError(t, os.WriteFile(themeFile, []byte("ok"), 0o600))
// Create a registry node via FileRegistry.
r, err := reg.NewClientRegistryWithConfig(c)
assert.NoError(t, err)
n := &reg.Node{Name: "pp-node-01", Role: "instance", Labels: map[string]string{"env": "test"}}
assert.NoError(t, r.Put(n))
// nodes ls (JSON)
out, err := RunWithTestContext(ClusterNodesListCommand, []string{"ls", "--json"})
assert.NoError(t, err)
assert.Contains(t, out, "pp-node-01")
// nodes show by name
out, err = RunWithTestContext(ClusterNodesShowCommand, []string{"show", "pp-node-01"})
assert.NoError(t, err)
assert.Contains(t, out, "pp-node-01")
// nodes mod: add another label (non-interactive)
out, err = RunWithTestContext(ClusterNodesModCommand, []string{"mod", "pp-node-01", "--label", "region=us-east-1", "-y"})
assert.NoError(t, err)
_ = out
// theme pull via HTTP: fake portal endpoint returns a zip with test.txt
// Prepare temp destination
destDir := t.TempDir()
// Create a fake portal theme zip server
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/v1/cluster/theme" {
http.NotFound(w, r)
return
}
if r.Header.Get("Authorization") != "Bearer test-token" {
w.WriteHeader(http.StatusUnauthorized)
return
}
w.Header().Set("Content-Type", "application/zip")
// Build a small zip in-memory
var buf bytes.Buffer
zw := zip.NewWriter(&buf)
f, _ := zw.Create("test.txt")
_, _ = f.Write([]byte("ok"))
_ = zw.Close()
_, _ = w.Write(buf.Bytes())
}))
defer ts.Close()
_ = os.Setenv("PHOTOPRISM_PORTAL_URL", ts.URL)
_ = os.Setenv("PHOTOPRISM_JOIN_TOKEN", "test-token")
defer os.Unsetenv("PHOTOPRISM_PORTAL_URL")
defer os.Unsetenv("PHOTOPRISM_JOIN_TOKEN")
out, err = RunWithTestContext(ClusterThemePullCommand.Subcommands[0], []string{"pull", "--dest", destDir, "-f", "--portal-url=" + ts.URL, "--join-token=test-token"})
assert.NoError(t, err)
// Expect extracted file
assert.FileExists(t, filepath.Join(destDir, "test.txt"))
}