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 := ®.Node{Name: "pp-node-01", Role: "instance", UUID: rnd.UUIDv7()} assert.NoError(t, regy.Put(n)) n2 := ®.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 := ®.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) }