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" ) func TestClusterEndpoints(t *testing.T) { app, router, conf := NewApiTest() conf.Options().NodeType = cluster.Portal 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.NewFileRegistry(conf) assert.NoError(t, err) n := ®.Node{ID: "n1", Name: "pp-node-01", Type: "instance"} assert.NoError(t, regy.Put(n)) n2 := ®.Node{ID: "n2", Name: "pp-node-02", Type: "service"} assert.NoError(t, regy.Put(n2)) // Get by id r = PerformRequest(app, http.MethodGet, "/api/v1/cluster/nodes/n1") 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/n1", `{"internalUrl":"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 r = PerformRequest(app, http.MethodDelete, "/api/v1/cluster/nodes/n1") assert.Equal(t, http.StatusOK, 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 :id path parameter and rejects unsafe values. func TestClusterGetNode_IDValidation(t *testing.T) { app, router, conf := NewApiTest() conf.Options().NodeType = cluster.Portal // Register route under test. ClusterGetNode(router) // Seed a node with a simple, valid id. regy, err := reg.NewFileRegistry(conf) assert.NoError(t, err) n := ®.Node{ID: "n1", Name: "pp-node-99", Type: "instance"} assert.NoError(t, regy.Put(n)) // Valid ID returns 200. r := PerformRequest(app, http.MethodGet, "/api/v1/cluster/nodes/n1") 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) }