From a3eb27ad19182e89d0e36f9b573acfd47d8fba31 Mon Sep 17 00:00:00 2001 From: Eslam-Nawara Date: Wed, 11 Jun 2025 16:16:17 +0300 Subject: [PATCH 1/7] add approve nodes endpoint --- node-registrar/pkg/db/farms.go | 28 +++++++++++++ node-registrar/pkg/db/models.go | 5 ++- node-registrar/pkg/db/nodes.go | 9 +++++ node-registrar/pkg/server/handlers.go | 57 +++++++++++++++++++++++++++ node-registrar/pkg/server/routes.go | 1 + 5 files changed, 98 insertions(+), 2 deletions(-) diff --git a/node-registrar/pkg/db/farms.go b/node-registrar/pkg/db/farms.go index e4057bb..a9e61c8 100644 --- a/node-registrar/pkg/db/farms.go +++ b/node-registrar/pkg/db/farms.go @@ -1,6 +1,7 @@ package db import ( + "fmt" "strings" "github.com/pkg/errors" @@ -71,3 +72,30 @@ func (db *Database) UpdateFarm(farmID uint64, name string, stellarAddr string) ( } return nil } + +// ApproveNodes approves multiple nodes for a specific farm +func (db *Database) ApproveNodes(farmID uint64, nodeIDs []uint64) error { + // Start a transaction to ensure all updates are atomic + tx := db.gormDB.Begin() + if tx.Error != nil { + return tx.Error + } + + result := tx.Model(&Node{}). + Where("farm_id = ? AND node_id IN ? AND approved = ?", farmID, nodeIDs, false). + Update("approved", true) + + if result.Error != nil { + tx.Rollback() + return result.Error + } + + // Check if all nodes were found and updated + if int(result.RowsAffected) != len(nodeIDs) { + tx.Rollback() + return fmt.Errorf("some nodes were not found, do not belong to farm %d, or are already approved", farmID) + } + + // Commit the transaction + return tx.Commit().Error +} diff --git a/node-registrar/pkg/db/models.go b/node-registrar/pkg/db/models.go index ee98059..dc412a9 100644 --- a/node-registrar/pkg/db/models.go +++ b/node-registrar/pkg/db/models.go @@ -102,10 +102,11 @@ type NodeFilter struct { NodeID *uint64 `form:"node_id"` FarmID *uint64 `form:"farm_id"` TwinID *uint64 `form:"twin_id"` - Status string `form:"status"` - Healthy bool `form:"healthy"` + Status *string `form:"status"` + Healthy *bool `form:"healthy"` Online *bool `form:"online"` // Filter by online status (true = online, false = offline, nil = both) LastSeen *int64 `form:"last_seen"` // Filter nodes last seen within this many minutes + Approved *bool `form:"approved"` } type FarmFilter struct { diff --git a/node-registrar/pkg/db/nodes.go b/node-registrar/pkg/db/nodes.go index abb6ee1..e008406 100644 --- a/node-registrar/pkg/db/nodes.go +++ b/node-registrar/pkg/db/nodes.go @@ -22,6 +22,15 @@ func (db *Database) ListNodes(filter NodeFilter, limit Limit) (nodes []Node, err if filter.TwinID != nil { query = query.Where("twin_id = ?", *filter.TwinID) } + if filter.Healthy != nil { + query = query.Where("healthy= ?", *filter.Healthy) + } + if filter.Status != nil { + query = query.Where("status= ?", *filter.Status) + } + if filter.Approved != nil { + query = query.Where("approved= ?", *filter.Approved) + } // Filter by online status (node sent an uptime report in the last 30 minutes) if filter.Online != nil { diff --git a/node-registrar/pkg/server/handlers.go b/node-registrar/pkg/server/handlers.go index 86b6dff..e7b8e3d 100644 --- a/node-registrar/pkg/server/handlers.go +++ b/node-registrar/pkg/server/handlers.go @@ -825,3 +825,60 @@ func validateTimestampHint(timestampHint int64) error { return nil } + +type ApproveNodesRequest struct { + NodeIDs []uint64 `json:"node_ids" binding:"required,min=1"` +} + +// @Summary Approve nodes for a farm +// @Description Approve a list of nodes for a specific farm +// @Tags farms +// @Accept json +// @Produce json +// @Param X-Auth header string true "Authentication format: Base64(:):Base64(signature)" +// @Param farm_id path int true "Farm ID" +// @Param request body ApproveNodesRequest true "List of node IDs to approve" +// @Success 200 {object} map[string]any "Nodes approved successfully" +// @Failure 400 {object} map[string]any "Invalid request" +// @Failure 401 {object} map[string]any "Unauthorized" +// @Failure 404 {object} map[string]any "Farm not found" +// @Router /farms/{farm_id}/approve [post] +func (s *Server) approveNodesHandler(c *gin.Context) { + farmID := c.Param("farm_id") + + id, err := strconv.ParseUint(farmID, 10, 64) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Invalid farm_id: %v", err.Error())}) + return + } + + // Verify farm exists and get owner + farm, err := s.db.GetFarm(id) + if err != nil { + if errors.Is(err, db.ErrRecordNotFound) { + c.JSON(http.StatusNotFound, gin.H{"error": "Farm not found"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // Ensure the requester owns the farm + ensureOwner(c, farm.TwinID) + if c.IsAborted() { + return + } + + var req ApproveNodesRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if err := s.db.ApproveNodes(id, req.NodeIDs); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Nodes approved successfully"}) +} diff --git a/node-registrar/pkg/server/routes.go b/node-registrar/pkg/server/routes.go index 0e4b547..a422df3 100644 --- a/node-registrar/pkg/server/routes.go +++ b/node-registrar/pkg/server/routes.go @@ -38,6 +38,7 @@ func (s *Server) registerRoutes(r *gin.RouterGroup) { // added to stop redirecting when creating a farm with extra / protectedFarmRoutes.POST("/", s.createFarmHandler) protectedFarmRoutes.PATCH("/:farm_id", s.updateFarmHandler) + protectedFarmRoutes.POST("/:farm_id/approve", s.approveNodesHandler) // nodes routes publicNodeRoutes := r.Group("nodes") From 18b2a0385d98604532c155b058a27e68b0ec1833 Mon Sep 17 00:00:00 2001 From: Eslam-Nawara Date: Wed, 11 Jun 2025 16:23:13 +0300 Subject: [PATCH 2/7] add documentation for approve endpoint --- node-registrar/README.md | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/node-registrar/README.md b/node-registrar/README.md index 12be537..7d5654c 100644 --- a/node-registrar/README.md +++ b/node-registrar/README.md @@ -1,4 +1,3 @@ - # Node Registrar Service [![Go Report Card](https://goreportcard.com/badge/github.com/threefoldtech/tfgrid-sdk-go/node-registrar)](https://goreportcard.com/report/github.com/threefoldtech/tfgrid-sdk-go/node-registrar) @@ -40,6 +39,7 @@ It offers operations like registring, listing, and updating farms and nodes, as | GET | `/farms/:farm_id` | Get a specific farm by ID | | POST | `/farms/` | Create a new farm | | PATCH | `/farms/` | Update an existing farm | +| POST | `/farms/:farm_id/approve` | Approve multiple nodes for a specific farm | ### Nodes Endpoints @@ -51,6 +51,27 @@ It offers operations like registring, listing, and updating farms and nodes, as | POST | `/nodes/:node_id/uptime` | Report uptime for a specific node | | POST | `/nodes/:node_id/consumption` | Report consumption for a specific node | +### Node Approval + +The `/farms/:farm_id/approve` allows farmers to approve multiple nodes at once. This endpoint: + +- Requires farm owner authentication +- Accepts a list of node IDs in the request body +- Only approves nodes that: + - Belong to the specified farm + - Are currently not approved + +Example request: + +```json +POST /farms/123/approve +{ + "node_ids": [1, 2, 3] +} +``` + +The operation is atomic - either all nodes are approved or none are. If any node cannot be approved (not found, already approved, or doesn't belong to the farm), the entire operation fails. + ## Setup Instructions 1. **Start PostgreSQL:** @@ -94,7 +115,7 @@ Replace `` and `` with the appropriate values. docker build -t registrar:latest -f node-registrar/Dockerfile . ``` -2. run the image +1. run the image ```bash docker run -d \ From 57dbb38ecf5dc2f0434a55cc557bbc496f8981ae Mon Sep 17 00:00:00 2001 From: Eslam-Nawara Date: Thu, 12 Jun 2025 13:57:30 +0300 Subject: [PATCH 3/7] add client function to approve nodes --- node-registrar/client/farm.go | 56 +++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/node-registrar/client/farm.go b/node-registrar/client/farm.go index 88fc1a8..fa12eec 100644 --- a/node-registrar/client/farm.go +++ b/node-registrar/client/farm.go @@ -35,6 +35,62 @@ func (c *RegistrarClient) ListFarms(filter FarmFilter) (farms []Farm, err error) return c.listFarms(filter) } +// ApproveNodes approves multiple nodes for a specific farm +func (c *RegistrarClient) ApproveNodes(farmID uint64, nodeIDs []uint64) error { + return c.approveNodes(farmID, nodeIDs) +} + +func (c *RegistrarClient) approveNodes(farmID uint64, nodeIDs []uint64) error { + if err := c.ensureTwinID(); err != nil { + return errors.Wrap(err, "failed to ensure twin id") + } + + url, err := url.JoinPath(c.baseURL, "farms", fmt.Sprint(farmID), "approve") + if err != nil { + return errors.Wrap(err, "failed to construct registrar url") + } + + data := struct { + NodeIDs []uint64 `json:"node_ids"` + }{ + NodeIDs: nodeIDs, + } + + var body bytes.Buffer + if err = json.NewEncoder(&body).Encode(data); err != nil { + return errors.Wrap(err, "failed to encode request body") + } + + req, err := http.NewRequest("POST", url, &body) + if err != nil { + return errors.Wrap(err, "failed to construct http request to the registrar") + } + + authHeader, err := c.signRequest(time.Now().Unix()) + if err != nil { + return errors.Wrap(err, "failed to sign request") + } + req.Header.Set("X-Auth", authHeader) + req.Header.Set("Content-Type", "application/json") + + resp, err := c.httpClient.Do(req) + if err != nil { + return errors.Wrap(err, "failed to send request to approve nodes") + } + + if resp == nil { + return errors.New("failed to approve nodes, no response received") + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + err = parseResponseError(resp.Body) + return errors.Wrapf(err, "failed to approve nodes with status code %s", resp.Status) + } + + return nil +} + // FarmUpdate represents the data needed to update an existing farm type FarmUpdate struct { FarmName *string From 7ad8785a0b8c5be42b2415dc1008e3970a8baba1 Mon Sep 17 00:00:00 2001 From: Eslam-Nawara Date: Thu, 12 Jun 2025 13:57:46 +0300 Subject: [PATCH 4/7] add ListUnapprovedNodes --- node-registrar/client/node.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/node-registrar/client/node.go b/node-registrar/client/node.go index 5d5eade..a339732 100644 --- a/node-registrar/client/node.go +++ b/node-registrar/client/node.go @@ -45,6 +45,17 @@ func (c *RegistrarClient) ListNodes(opts NodeFilter) (nodes []Node, err error) { return c.listNodesWithFilter(opts) } +// ListUnapprovedNodes gets a list of unapproved nodes for a specific farm +func (c *RegistrarClient) ListUnapprovedNodes(farmID uint64) ([]Node, error) { + falseVal := false + filter := NodeFilter{ + FarmID: &farmID, + Approved: &falseVal, + } + + return c.ListNodes(filter) +} + func (c *RegistrarClient) registerNode(node Node) (nodeID uint64, err error) { err = c.ensureTwinID() if err != nil { @@ -331,6 +342,7 @@ type NodeFilter struct { LastSeen *int64 Page *uint32 Size *uint32 + Approved *bool } func (c *RegistrarClient) listNodesWithFilter(filter NodeFilter) (nodes []Node, err error) { From 9c889eb8b85eeec5601c58cb126d66a3091bc0e9 Mon Sep 17 00:00:00 2001 From: Eslam-Nawara Date: Thu, 12 Jun 2025 17:22:15 +0300 Subject: [PATCH 5/7] add tests for approval mechanism --- node-registrar/client/farm_test.go | 46 +++++++++++++++++++++++++++++ node-registrar/client/node.go | 3 ++ node-registrar/client/node_test.go | 29 ++++++++++++++++++ node-registrar/client/utils_test.go | 44 +++++++++++++++++++++++++++ 4 files changed, 122 insertions(+) diff --git a/node-registrar/client/farm_test.go b/node-registrar/client/farm_test.go index d02baf6..1d8f7ba 100644 --- a/node-registrar/client/farm_test.go +++ b/node-registrar/client/farm_test.go @@ -134,3 +134,49 @@ func TestGetFarm(t *testing.T) { require.Equal(result, farm) }) } + +func TestApproveNodes(t *testing.T) { + var request int + var count int + require := require.New(t) + + keyPair, err := parseKeysFromMnemonicOrSeed(testMnemonic) + require.NoError(err) + account.PublicKey = base64.StdEncoding.EncodeToString(keyPair.Public()) + + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + statusCode, body := serverHandler(r, request, count, require) + w.WriteHeader(statusCode) + _, err := w.Write(body) + require.NoError(err) + count++ + })) + defer testServer.Close() + + baseURL, err := url.JoinPath(testServer.URL, "v1") + require.NoError(err) + + t.Run("test approve nodes with status unauthorized", func(t *testing.T) { + count = 0 + request = newClientWithNoAccount + c, err := NewRegistrarClient(baseURL, testMnemonic) + require.NoError(err) + + count = 0 + request = approveNodesWithStatusUnauthorized + err = c.ApproveNodes(farmID, []uint64{1, 2, 3}) + require.Error(err) + }) + + t.Run("test approve nodes with status ok", func(t *testing.T) { + count = 0 + request = newClientWithAccountNoNode + c, err := NewRegistrarClient(baseURL, testMnemonic) + require.NoError(err) + + count = 0 + request = approveNodesWithStatusOK + err = c.ApproveNodes(farmID, []uint64{1, 2, 3}) + require.NoError(err) + }) +} diff --git a/node-registrar/client/node.go b/node-registrar/client/node.go index a339732..c596f06 100644 --- a/node-registrar/client/node.go +++ b/node-registrar/client/node.go @@ -496,6 +496,9 @@ func parseListNodeOpts(filter NodeFilter) map[string]any { if filter.LastSeen != nil { data["last_seen"] = *filter.LastSeen } + if filter.Approved != nil { + data["approved"] = *filter.Approved + } page := uint32(1) if filter.Page != nil { diff --git a/node-registrar/client/node_test.go b/node-registrar/client/node_test.go index b59d356..e58fad0 100644 --- a/node-registrar/client/node_test.go +++ b/node-registrar/client/node_test.go @@ -166,3 +166,32 @@ func TestGetNode(t *testing.T) { require.Equal([]Node{node}, result) }) } + +func TestListUnapprovedNodes(t *testing.T) { + var request int + var count int + require := require.New(t) + + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + statusCode, body := serverHandler(r, request, count, require) + w.WriteHeader(statusCode) + _, err := w.Write(body) + require.NoError(err) + count++ + })) + defer testServer.Close() + + baseURL, err := url.JoinPath(testServer.URL, "v1") + require.NoError(err) + + t.Run("test list unapproved nodes in farm", func(t *testing.T) { + request = newClientWithAccountNoNode + c, err := NewRegistrarClient(baseURL, testMnemonic) + require.NoError(err) + + request = listUnapprovedNodesInFarm + nodes, err := c.ListUnapprovedNodes(farmID) + require.NoError(err) + require.Equal([]Node{node}, nodes) + }) +} diff --git a/node-registrar/client/utils_test.go b/node-registrar/client/utils_test.go index 80b5d1a..cc81c1b 100644 --- a/node-registrar/client/utils_test.go +++ b/node-registrar/client/utils_test.go @@ -33,6 +33,8 @@ const ( updateFarmWithStatusUnauthorized getFarmWithStatusNotfound getFarmWithStatusOK + approveNodesWithStatusOK + approveNodesWithStatusUnauthorized registerNodeStatusCreated registerNodeWithNoAccount @@ -43,6 +45,7 @@ const ( getNodeWithIDStatusNotFound getNodeWithTwinID listNodesInFarm + listUnapprovedNodesInFarm testMnemonic = "bottom drive obey lake curtain smoke basket hold race lonely fit walk" @@ -220,6 +223,47 @@ func serverHandler(r *http.Request, request, count int, require *require.Asserti require.NoError(err) return http.StatusOK, resp + case approveNodesWithStatusOK: + require.Equal("/v1/farms/1/approve", r.URL.Path) + require.Equal(http.MethodPost, r.Method) + require.NotEmpty(r.Body) + var body struct { + NodeIDs []uint64 `json:"node_ids"` + } + err := json.NewDecoder(r.Body).Decode(&body) + require.NoError(err) + require.Equal([]uint64{1, 2, 3}, body.NodeIDs) + return http.StatusOK, nil + + case approveNodesWithStatusUnauthorized: + switch count { + case 0: + require.Equal("/v1/accounts", r.URL.Path) + require.Equal(account.PublicKey, r.URL.Query().Get("public_key")) + require.Equal(http.MethodGet, r.Method) + return http.StatusNotFound, nil + case 1: + require.Equal("/v1/farms/1/approve", r.URL.Path) + require.Equal(http.MethodPost, r.Method) + require.NotEmpty(r.Body) + var body struct { + NodeIDs []uint64 `json:"node_ids"` + } + err := json.NewDecoder(r.Body).Decode(&body) + require.NoError(err) + require.Equal([]uint64{1, 2, 3}, body.NodeIDs) + return http.StatusUnauthorized, nil + } + + case listUnapprovedNodesInFarm: + require.Equal("/v1/nodes", r.URL.Path) + require.Equal(fmt.Sprint(farmID), r.URL.Query().Get("farm_id")) + require.Equal("false", r.URL.Query().Get("approved")) + require.Equal(http.MethodGet, r.Method) + resp, err := json.Marshal([]Node{node}) + require.NoError(err) + return http.StatusOK, resp + // unauthorized requests case newClientWithNoAccount, getAccountWithPKStatusNotFount, From 43e52dcd962e54d616f13654d33629c4482164c7 Mon Sep 17 00:00:00 2001 From: Eslam-Nawara Date: Sun, 13 Jul 2025 16:16:30 +0300 Subject: [PATCH 6/7] fix tests --- node-registrar/client/account_test.go | 6 +-- node-registrar/client/client_test.go | 2 +- node-registrar/client/farm_test.go | 12 ++--- node-registrar/client/node.go | 4 +- node-registrar/client/node_test.go | 31 +++++++---- node-registrar/client/utils_test.go | 78 ++++++++++++--------------- 6 files changed, 66 insertions(+), 67 deletions(-) diff --git a/node-registrar/client/account_test.go b/node-registrar/client/account_test.go index 798ae4f..1e2b98d 100644 --- a/node-registrar/client/account_test.go +++ b/node-registrar/client/account_test.go @@ -28,7 +28,7 @@ func TestCreateAccount(t *testing.T) { })) defer testServer.Close() - baseURL, err := url.JoinPath(testServer.URL, "v1") + baseURL, err := url.JoinPath(testServer.URL, "api", "v1") require.NoError(err) request = newClientWithNoAccount @@ -62,7 +62,7 @@ func TestUpdateAccount(t *testing.T) { })) defer testServer.Close() - baseURL, err := url.JoinPath(testServer.URL, "v1") + baseURL, err := url.JoinPath(testServer.URL, "api", "v1") require.NoError(err) t.Run("test update account updated successfully", func(t *testing.T) { @@ -112,7 +112,7 @@ func TestGetAccount(t *testing.T) { })) defer testServer.Close() - baseURL, err := url.JoinPath(testServer.URL, "v1") + baseURL, err := url.JoinPath(testServer.URL, "api", "v1") require.NoError(err) count = 0 diff --git a/node-registrar/client/client_test.go b/node-registrar/client/client_test.go index 0a2a973..a5cea2f 100644 --- a/node-registrar/client/client_test.go +++ b/node-registrar/client/client_test.go @@ -28,7 +28,7 @@ func TestNewRegistrarClient(t *testing.T) { })) defer testServer.Close() - baseURL, err := url.JoinPath(testServer.URL, "v1") + baseURL, err := url.JoinPath(testServer.URL, "api", "v1") require.NoError(err) t.Run("test new registrar client with no account", func(t *testing.T) { diff --git a/node-registrar/client/farm_test.go b/node-registrar/client/farm_test.go index 1d8f7ba..93e026e 100644 --- a/node-registrar/client/farm_test.go +++ b/node-registrar/client/farm_test.go @@ -28,7 +28,7 @@ func TestCreateFarm(t *testing.T) { })) defer testServer.Close() - baseURL, err := url.JoinPath(testServer.URL, "v1") + baseURL, err := url.JoinPath(testServer.URL, "api", "v1") require.NoError(err) request = newClientWithAccountNoNode @@ -68,7 +68,7 @@ func TestUpdateFarm(t *testing.T) { })) defer testServer.Close() - baseURL, err := url.JoinPath(testServer.URL, "v1") + baseURL, err := url.JoinPath(testServer.URL, "api", "v1") require.NoError(err) t.Run("test update farm with status unauthorzed", func(t *testing.T) { @@ -113,7 +113,7 @@ func TestGetFarm(t *testing.T) { })) defer testServer.Close() - baseURL, err := url.JoinPath(testServer.URL, "v1") + baseURL, err := url.JoinPath(testServer.URL, "api", "v1") require.NoError(err) count = 0 @@ -153,16 +153,15 @@ func TestApproveNodes(t *testing.T) { })) defer testServer.Close() - baseURL, err := url.JoinPath(testServer.URL, "v1") + baseURL, err := url.JoinPath(testServer.URL, "api", "v1") require.NoError(err) t.Run("test approve nodes with status unauthorized", func(t *testing.T) { count = 0 - request = newClientWithNoAccount + request = newClientWithAccountNoNode c, err := NewRegistrarClient(baseURL, testMnemonic) require.NoError(err) - count = 0 request = approveNodesWithStatusUnauthorized err = c.ApproveNodes(farmID, []uint64{1, 2, 3}) require.Error(err) @@ -174,7 +173,6 @@ func TestApproveNodes(t *testing.T) { c, err := NewRegistrarClient(baseURL, testMnemonic) require.NoError(err) - count = 0 request = approveNodesWithStatusOK err = c.ApproveNodes(farmID, []uint64{1, 2, 3}) require.NoError(err) diff --git a/node-registrar/client/node.go b/node-registrar/client/node.go index c596f06..64f00c6 100644 --- a/node-registrar/client/node.go +++ b/node-registrar/client/node.go @@ -160,7 +160,7 @@ func (c *RegistrarClient) updateNode(opts NodeUpdate) (err error) { return errors.Wrap(err, "failed to construct registrar url") } - node = c.parseUpdateNodeOpts(node, opts) + node = parseUpdateNodeOpts(node, opts) handler := func(body bytes.Buffer) error { req, err := http.NewRequest("PATCH", url, &body) @@ -437,7 +437,7 @@ func (c *RegistrarClient) ensureNodeID() error { return nil } -func (c *RegistrarClient) parseUpdateNodeOpts(node Node, update NodeUpdate) Node { +func parseUpdateNodeOpts(node Node, update NodeUpdate) Node { if update.FarmID != nil { node.FarmID = *update.FarmID } diff --git a/node-registrar/client/node_test.go b/node-registrar/client/node_test.go index e58fad0..d77bae4 100644 --- a/node-registrar/client/node_test.go +++ b/node-registrar/client/node_test.go @@ -1,6 +1,7 @@ package client import ( + "encoding/base64" "net/http" "net/http/httptest" "net/url" @@ -20,6 +21,10 @@ func TestRegistarNode(t *testing.T) { FarmID: farmID, } + keyPair, err := parseKeysFromMnemonicOrSeed(testMnemonic) + require.NoError(err) + account.PublicKey = base64.StdEncoding.EncodeToString(keyPair.Public()) + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { statusCode, body := serverHandler(r, request, count, require) w.WriteHeader(statusCode) @@ -29,7 +34,7 @@ func TestRegistarNode(t *testing.T) { })) defer testServer.Close() - baseURL, err := url.JoinPath(testServer.URL, "v1") + baseURL, err := url.JoinPath(testServer.URL, "api", "v1") require.NoError(err) t.Run("test registar node no account", func(t *testing.T) { @@ -73,10 +78,10 @@ func TestUpdateNode(t *testing.T) { var count int require := require.New(t) - // publicKey, privateKey, err := aliceKeys() - // require.NoError(err) - // account.PublicKey = base64.StdEncoding.EncodeToString(publicKey) - // + keyPair, err := parseKeysFromMnemonicOrSeed(testMnemonic) + require.NoError(err) + account.PublicKey = base64.StdEncoding.EncodeToString(keyPair.Public()) + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { statusCode, body := serverHandler(r, request, count, require) w.WriteHeader(statusCode) @@ -86,7 +91,7 @@ func TestUpdateNode(t *testing.T) { })) defer testServer.Close() - baseURL, err := url.JoinPath(testServer.URL, "v1") + baseURL, err := url.JoinPath(testServer.URL, "api", "v1") require.NoError(err) request = newClientWithAccountAndNode @@ -118,9 +123,9 @@ func TestGetNode(t *testing.T) { var count int require := require.New(t) - // publicKey, privateKey, err := aliceKeys() - // require.NoError(err) - // account.PublicKey = base64.StdEncoding.EncodeToString(publicKey) + keyPair, err := parseKeysFromMnemonicOrSeed(testMnemonic) + require.NoError(err) + account.PublicKey = base64.StdEncoding.EncodeToString(keyPair.Public()) testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { statusCode, body := serverHandler(r, request, count, require) @@ -131,7 +136,7 @@ func TestGetNode(t *testing.T) { })) defer testServer.Close() - baseURL, err := url.JoinPath(testServer.URL, "v1") + baseURL, err := url.JoinPath(testServer.URL, "api", "v1") require.NoError(err) request = newClientWithAccountAndNode @@ -172,6 +177,10 @@ func TestListUnapprovedNodes(t *testing.T) { var count int require := require.New(t) + keyPair, err := parseKeysFromMnemonicOrSeed(testMnemonic) + require.NoError(err) + account.PublicKey = base64.StdEncoding.EncodeToString(keyPair.Public()) + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { statusCode, body := serverHandler(r, request, count, require) w.WriteHeader(statusCode) @@ -181,7 +190,7 @@ func TestListUnapprovedNodes(t *testing.T) { })) defer testServer.Close() - baseURL, err := url.JoinPath(testServer.URL, "v1") + baseURL, err := url.JoinPath(testServer.URL, "api", "v1") require.NoError(err) t.Run("test list unapproved nodes in farm", func(t *testing.T) { diff --git a/node-registrar/client/utils_test.go b/node-registrar/client/utils_test.go index cc81c1b..ec65fe3 100644 --- a/node-registrar/client/utils_test.go +++ b/node-registrar/client/utils_test.go @@ -60,14 +60,14 @@ func serverHandler(r *http.Request, request, count int, require *require.Asserti case newClientWithAccountNoNode: switch count { case 0: - require.Equal("/v1/accounts", r.URL.Path) + require.Equal("/api/v1/accounts", r.URL.Path) require.Equal(account.PublicKey, r.URL.Query().Get("public_key")) require.Equal(http.MethodGet, r.Method) resp, err := json.Marshal(account) require.NoError(err) return http.StatusOK, resp case 1: - require.Equal("/v1/nodes", r.URL.Path) + require.Equal("/api/v1/nodes", r.URL.Path) require.Equal(fmt.Sprint(account.TwinID), r.URL.Query().Get("twin_id")) require.Equal(http.MethodGet, r.Method) return http.StatusNotFound, nil @@ -76,14 +76,14 @@ func serverHandler(r *http.Request, request, count int, require *require.Asserti case newClientWithAccountAndNode: switch count { case 0: - require.Equal("/v1/accounts", r.URL.Path) + require.Equal("/api/v1/accounts", r.URL.Path) require.Equal(account.PublicKey, r.URL.Query().Get("public_key")) require.Equal(http.MethodGet, r.Method) resp, err := json.Marshal(account) require.NoError(err) return http.StatusOK, resp case 1: - require.Equal("/v1/nodes", r.URL.Path) + require.Equal("/api/v1/nodes", r.URL.Path) require.Equal(fmt.Sprint(account.TwinID), r.URL.Query().Get("twin_id")) require.Equal(http.MethodGet, r.Method) resp, err := json.Marshal([]Node{node}) @@ -93,7 +93,7 @@ func serverHandler(r *http.Request, request, count int, require *require.Asserti // Accounts routes handlers case createAccountStatusCreated: - require.Equal("/v1/accounts", r.URL.Path) + require.Equal("/api/v1/accounts", r.URL.Path) require.Equal(http.MethodPost, r.Method) require.NotEmpty(r.Body) resp, err := json.Marshal(account) @@ -101,7 +101,7 @@ func serverHandler(r *http.Request, request, count int, require *require.Asserti return http.StatusCreated, resp case getAccountWithPKStatusOK: - require.Equal("/v1/accounts", r.URL.Path) + require.Equal("/api/v1/accounts", r.URL.Path) require.Equal(account.PublicKey, r.URL.Query().Get("public_key")) require.Equal(http.MethodGet, r.Method) resp, err := json.Marshal(account) @@ -109,13 +109,13 @@ func serverHandler(r *http.Request, request, count int, require *require.Asserti return http.StatusOK, resp case getAccountWithIDStatusNotFount: - require.Equal("/v1/accounts", r.URL.Path) + require.Equal("/api/v1/accounts", r.URL.Path) require.Equal(fmt.Sprint(account.TwinID), r.URL.Query().Get("twin_id")) require.Equal(http.MethodGet, r.Method) return http.StatusNotFound, nil case getAccountWithIDStatusOK: - require.Equal("/v1/accounts", r.URL.Path) + require.Equal("/api/v1/accounts", r.URL.Path) require.Equal(fmt.Sprint(account.TwinID), r.URL.Query().Get("twin_id")) require.Equal(http.MethodGet, r.Method) resp, err := json.Marshal(account) @@ -123,19 +123,19 @@ func serverHandler(r *http.Request, request, count int, require *require.Asserti return http.StatusOK, resp case updateAccountWithStatusOK: - require.Equal("/v1/accounts/1", r.URL.Path) + require.Equal("/api/v1/accounts/1", r.URL.Path) require.Equal(http.MethodPatch, r.Method) return http.StatusOK, nil // Farm routes handlers case createFarmStatusConflict: - require.Equal("/v1/farms", r.URL.Path) + require.Equal("/api/v1/farms", r.URL.Path) require.Equal(http.MethodPost, r.Method) require.NotEmpty(r.Body) return http.StatusConflict, nil case createFarmStatusCreated: - require.Equal("/v1/farms", r.URL.Path) + require.Equal("/api/v1/farms", r.URL.Path) require.Equal(http.MethodPost, r.Method) require.NotEmpty(r.Body) resp, err := json.Marshal(map[string]uint64{"farm_id": farmID}) @@ -143,31 +143,31 @@ func serverHandler(r *http.Request, request, count int, require *require.Asserti return http.StatusCreated, resp case updateFarmWithStatusOK: - require.Equal("/v1/farms/1", r.URL.Path) + require.Equal("/api/v1/farms/1", r.URL.Path) require.Equal(http.MethodPatch, r.Method) require.NotEmpty(r.Body) return http.StatusOK, nil case getFarmWithStatusOK: - require.Equal("/v1/farms/1", r.URL.Path) + require.Equal("/api/v1/farms/1", r.URL.Path) require.Equal(http.MethodGet, r.Method) resp, err := json.Marshal(farm) require.NoError(err) return http.StatusOK, resp case getFarmWithStatusNotfound: - require.Equal("/v1/farms/1", r.URL.Path) + require.Equal("/api/v1/farms/1", r.URL.Path) require.Equal(http.MethodGet, r.Method) return http.StatusNotFound, nil // Node routes handlers case registerNodeStatusConflict: - require.Equal("/v1/nodes", r.URL.Path) + require.Equal("/api/v1/nodes", r.URL.Path) require.Equal(http.MethodPost, r.Method) return http.StatusConflict, nil case registerNodeStatusCreated: - require.Equal("/v1/nodes", r.URL.Path) + require.Equal("/api/v1/nodes", r.URL.Path) require.Equal(http.MethodPost, r.Method) require.NotEmpty(r.Body) resp, err := json.Marshal(map[string]uint64{"node_id": nodeID}) @@ -177,38 +177,38 @@ func serverHandler(r *http.Request, request, count int, require *require.Asserti case updateNodeStatusOK: switch count { case 0: - require.Equal("/v1/nodes/1", r.URL.Path) + require.Equal("/api/v1/nodes/1", r.URL.Path) require.Equal(http.MethodGet, r.Method) resp, err := json.Marshal(node) require.NoError(err) return http.StatusOK, resp case 1: - require.Equal("/v1/nodes/1", r.URL.Path) + require.Equal("/api/v1/nodes/1", r.URL.Path) require.Equal(http.MethodPatch, r.Method) require.NotEmpty(r.Body) return http.StatusOK, nil } case updateNodeSendUptimeReport: - require.Equal("/v1/nodes/1/uptime", r.URL.Path) + require.Equal("/api/v1/nodes/1/uptime", r.URL.Path) require.Equal(http.MethodPost, r.Method) require.NotEmpty(r.Body) return http.StatusCreated, nil case getNodeWithIDStatusOK: - require.Equal("/v1/nodes/1", r.URL.Path) + require.Equal("/api/v1/nodes/1", r.URL.Path) require.Equal(http.MethodGet, r.Method) resp, err := json.Marshal(node) require.NoError(err) return http.StatusOK, resp case getNodeWithIDStatusNotFound: - require.Equal("/v1/nodes/1", r.URL.Path) + require.Equal("/api/v1/nodes/1", r.URL.Path) require.Equal(http.MethodGet, r.Method) return http.StatusNotFound, nil case getNodeWithTwinID: - require.Equal("/v1/nodes", r.URL.Path) + require.Equal("/api/v1/nodes", r.URL.Path) require.Equal(fmt.Sprint(account.TwinID), r.URL.Query().Get("twin_id")) require.Equal(http.MethodGet, r.Method) resp, err := json.Marshal([]Node{node}) @@ -216,7 +216,7 @@ func serverHandler(r *http.Request, request, count int, require *require.Asserti return http.StatusOK, resp case listNodesInFarm: - require.Equal("/v1/nodes", r.URL.Path) + require.Equal("/api/v1/nodes", r.URL.Path) require.Equal(fmt.Sprint(farmID), r.URL.Query().Get("farm_id")) require.Equal(http.MethodGet, r.Method) resp, err := json.Marshal([]Node{node}) @@ -224,7 +224,7 @@ func serverHandler(r *http.Request, request, count int, require *require.Asserti return http.StatusOK, resp case approveNodesWithStatusOK: - require.Equal("/v1/farms/1/approve", r.URL.Path) + require.Equal("/api/v1/farms/1/approve", r.URL.Path) require.Equal(http.MethodPost, r.Method) require.NotEmpty(r.Body) var body struct { @@ -236,27 +236,19 @@ func serverHandler(r *http.Request, request, count int, require *require.Asserti return http.StatusOK, nil case approveNodesWithStatusUnauthorized: - switch count { - case 0: - require.Equal("/v1/accounts", r.URL.Path) - require.Equal(account.PublicKey, r.URL.Query().Get("public_key")) - require.Equal(http.MethodGet, r.Method) - return http.StatusNotFound, nil - case 1: - require.Equal("/v1/farms/1/approve", r.URL.Path) - require.Equal(http.MethodPost, r.Method) - require.NotEmpty(r.Body) - var body struct { - NodeIDs []uint64 `json:"node_ids"` - } - err := json.NewDecoder(r.Body).Decode(&body) - require.NoError(err) - require.Equal([]uint64{1, 2, 3}, body.NodeIDs) - return http.StatusUnauthorized, nil + require.Equal("/api/v1/farms/1/approve", r.URL.Path) + require.Equal(http.MethodPost, r.Method) + require.NotEmpty(r.Body) + var body struct { + NodeIDs []uint64 `json:"node_ids"` } + err := json.NewDecoder(r.Body).Decode(&body) + require.NoError(err) + require.Equal([]uint64{1, 2, 3}, body.NodeIDs) + return http.StatusUnauthorized, nil case listUnapprovedNodesInFarm: - require.Equal("/v1/nodes", r.URL.Path) + require.Equal("/api/v1/nodes", r.URL.Path) require.Equal(fmt.Sprint(farmID), r.URL.Query().Get("farm_id")) require.Equal("false", r.URL.Query().Get("approved")) require.Equal(http.MethodGet, r.Method) @@ -270,7 +262,7 @@ func serverHandler(r *http.Request, request, count int, require *require.Asserti updateAccountWithNoAccount, updateFarmWithStatusUnauthorized, registerNodeWithNoAccount: - require.Equal("/v1/accounts", r.URL.Path) + require.Equal("/api/v1/accounts", r.URL.Path) require.Equal(account.PublicKey, r.URL.Query().Get("public_key")) require.Equal(http.MethodGet, r.Method) return http.StatusNotFound, nil From 228c20df876f4cc103e5a1919f103e97eee1aec2 Mon Sep 17 00:00:00 2001 From: Eslam-Nawara Date: Sun, 13 Jul 2025 16:17:04 +0300 Subject: [PATCH 7/7] check if approve node has any nodes to approve before updating db --- node-registrar/pkg/db/farms.go | 2 +- node-registrar/pkg/server/handlers.go | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/node-registrar/pkg/db/farms.go b/node-registrar/pkg/db/farms.go index a9e61c8..04bcf7e 100644 --- a/node-registrar/pkg/db/farms.go +++ b/node-registrar/pkg/db/farms.go @@ -90,7 +90,7 @@ func (db *Database) ApproveNodes(farmID uint64, nodeIDs []uint64) error { return result.Error } - // Check if all nodes were found and updated + // Check if all nodes were found, belong to the requested farm and updated if int(result.RowsAffected) != len(nodeIDs) { tx.Rollback() return fmt.Errorf("some nodes were not found, do not belong to farm %d, or are already approved", farmID) diff --git a/node-registrar/pkg/server/handlers.go b/node-registrar/pkg/server/handlers.go index e7b8e3d..5d7b553 100644 --- a/node-registrar/pkg/server/handlers.go +++ b/node-registrar/pkg/server/handlers.go @@ -863,7 +863,7 @@ func (s *Server) approveNodesHandler(c *gin.Context) { return } - // Ensure the requester owns the farm + // Ensure the farmer approving the nodes is the owner of the farm ensureOwner(c, farm.TwinID) if c.IsAborted() { return @@ -875,6 +875,11 @@ func (s *Server) approveNodesHandler(c *gin.Context) { return } + if len(req.NodeIDs) == 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "could not find any nodes to approve"}) + return + } + if err := s.db.ApproveNodes(id, req.NodeIDs); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return