Files
photoprism/internal/api/cluster_nodes_test.go
2025-09-24 08:28:38 +02:00

123 lines
4.2 KiB
Go

package api
import (
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"github.com/photoprism/photoprism/internal/service/cluster"
reg "github.com/photoprism/photoprism/internal/service/cluster/registry"
"github.com/photoprism/photoprism/pkg/rnd"
)
func TestClusterEndpoints(t *testing.T) {
app, router, conf := NewApiTest()
conf.Options().NodeRole = cluster.RolePortal
ClusterListNodes(router)
ClusterGetNode(router)
ClusterUpdateNode(router)
ClusterDeleteNode(router)
// Empty list initially (JSON array)
r := PerformRequest(app, http.MethodGet, "/api/v1/cluster/nodes")
assert.Equal(t, http.StatusOK, r.Code)
// Seed nodes in the registry
regy, err := reg.NewClientRegistryWithConfig(conf)
assert.NoError(t, err)
n := &reg.Node{Name: "pp-node-01", Role: "instance", UUID: rnd.UUIDv7()}
assert.NoError(t, regy.Put(n))
n2 := &reg.Node{Name: "pp-node-02", Role: "service", UUID: rnd.UUIDv7()}
assert.NoError(t, regy.Put(n2))
// Resolve actual IDs (client-backed registry generates IDs)
n, err = regy.FindByName("pp-node-01")
assert.NoError(t, err)
// Get by UUID
r = PerformRequest(app, http.MethodGet, "/api/v1/cluster/nodes/"+n.UUID)
assert.Equal(t, http.StatusOK, r.Code)
// 404 for missing id
r = PerformRequest(app, http.MethodGet, "/api/v1/cluster/nodes/missing")
assert.Equal(t, http.StatusNotFound, r.Code)
// Patch (manage requires Auth; our Auth() in tests allows admin; skip strict role checks here)
r = PerformRequestWithBody(app, http.MethodPatch, "/api/v1/cluster/nodes/"+n.UUID, `{"advertiseUrl":"http://n1:2342"}`)
assert.Equal(t, http.StatusOK, r.Code)
// Pagination: count=1 returns exactly one
r = PerformRequest(app, http.MethodGet, "/api/v1/cluster/nodes?count=1")
assert.Equal(t, http.StatusOK, r.Code)
// Offset beyond length clamps to end and returns empty list
r = PerformRequest(app, http.MethodGet, "/api/v1/cluster/nodes?offset=10")
assert.Equal(t, http.StatusOK, r.Code)
// Delete existing
r = PerformRequest(app, http.MethodDelete, "/api/v1/cluster/nodes/"+n.UUID)
assert.Equal(t, http.StatusOK, r.Code)
// GET after delete -> 404
r = PerformRequest(app, http.MethodGet, "/api/v1/cluster/nodes/"+n.UUID)
assert.Equal(t, http.StatusNotFound, r.Code)
// DELETE nonexistent id -> 404
r = PerformRequest(app, http.MethodDelete, "/api/v1/cluster/nodes/missing-id")
assert.Equal(t, http.StatusNotFound, r.Code)
// DELETE invalid id (uppercase) -> 404
r = PerformRequest(app, http.MethodDelete, "/api/v1/cluster/nodes/BadID")
assert.Equal(t, http.StatusNotFound, r.Code)
// List again (should not include the deleted node)
r = PerformRequest(app, http.MethodGet, "/api/v1/cluster/nodes")
assert.Equal(t, http.StatusOK, r.Code)
}
// Test that ClusterGetNode validates the :uuid path parameter and rejects unsafe values.
func TestClusterGetNode_UUIDValidation(t *testing.T) {
app, router, conf := NewApiTest()
conf.Options().NodeRole = cluster.RolePortal
// Register route under test.
ClusterGetNode(router)
// Seed a node and resolve its actual ID.
regy, err := reg.NewClientRegistryWithConfig(conf)
assert.NoError(t, err)
n := &reg.Node{Name: "pp-node-99", Role: "instance", UUID: rnd.UUIDv7()}
assert.NoError(t, regy.Put(n))
n, err = regy.FindByName("pp-node-99")
assert.NoError(t, err)
// Valid UUID returns 200.
r := PerformRequest(app, http.MethodGet, "/api/v1/cluster/nodes/"+n.UUID)
assert.Equal(t, http.StatusOK, r.Code)
// Uppercase letters are not allowed.
r = PerformRequest(app, http.MethodGet, "/api/v1/cluster/nodes/N1")
assert.Equal(t, http.StatusNotFound, r.Code)
// Characters outside [a-z0-9-] are rejected (e.g., underscore).
r = PerformRequest(app, http.MethodGet, "/api/v1/cluster/nodes/bad_id")
assert.Equal(t, http.StatusNotFound, r.Code)
// Dot is rejected.
r = PerformRequest(app, http.MethodGet, "/api/v1/cluster/nodes/bad.id")
assert.Equal(t, http.StatusNotFound, r.Code)
// Encoded space is rejected.
r = PerformRequest(app, http.MethodGet, "/api/v1/cluster/nodes/a%20b")
assert.Equal(t, http.StatusNotFound, r.Code)
// Excessively long ID (>64 chars) is rejected.
longID := make([]byte, 65)
for i := range longID {
longID[i] = 'a'
}
r = PerformRequest(app, http.MethodGet, "/api/v1/cluster/nodes/"+string(longID))
assert.Equal(t, http.StatusNotFound, r.Code)
}