package mock import ( "context" "encoding/json" "fmt" "io" "math/rand" "net/http" "strings" "time" "spore-gateway/internal/discovery" "spore-gateway/pkg/client" "github.com/gorilla/mux" log "github.com/sirupsen/logrus" ) // MockHTTPServer represents the mock HTTP server type MockHTTPServer struct { port string router *mux.Router discovery *MockNodeDiscovery wsServer *MockWebSocketServer server *http.Server enableWS bool firmwareStore map[string][]byte // Simple in-memory firmware storage } // NewMockHTTPServer creates a new mock HTTP server instance func NewMockHTTPServer(port string, discovery *MockNodeDiscovery, enableWS bool) *MockHTTPServer { ms := &MockHTTPServer{ port: port, router: mux.NewRouter(), discovery: discovery, enableWS: enableWS, firmwareStore: make(map[string][]byte), } // Initialize WebSocket server if enabled if enableWS { ms.wsServer = NewMockWebSocketServer(discovery) } ms.setupRoutes() ms.setupMiddleware() ms.server = &http.Server{ Addr: ":" + port, Handler: ms.router, ReadTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second, IdleTimeout: 60 * time.Second, } return ms } // setupMiddleware configures middleware for the server func (ms *MockHTTPServer) setupMiddleware() { ms.router.Use(ms.corsMiddleware) ms.router.Use(ms.jsonMiddleware) ms.router.Use(ms.loggingMiddleware) } // corsMiddleware handles CORS headers func (ms *MockHTTPServer) corsMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, Accept") w.Header().Set("Access-Control-Expose-Headers", "Content-Type, Content-Length") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } next.ServeHTTP(w, r) }) } // jsonMiddleware sets JSON content type func (ms *MockHTTPServer) jsonMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") next.ServeHTTP(w, r) }) } // loggingMiddleware logs HTTP requests func (ms *MockHTTPServer) loggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() next.ServeHTTP(w, r) log.WithFields(log.Fields{ "method": r.Method, "path": r.URL.Path, "remote_addr": r.RemoteAddr, "duration": time.Since(start), }).Debug("HTTP request") }) } // setupRoutes configures all the API routes func (ms *MockHTTPServer) setupRoutes() { // API routes api := ms.router.PathPrefix("/api").Subrouter() api.Use(ms.corsMiddleware) // Discovery endpoints api.HandleFunc("/discovery/nodes", ms.getDiscoveryNodes).Methods("GET") api.HandleFunc("/discovery/refresh", ms.refreshDiscovery).Methods("POST", "OPTIONS") api.HandleFunc("/discovery/random-primary", ms.selectRandomPrimary).Methods("POST", "OPTIONS") api.HandleFunc("/discovery/primary/{ip}", ms.setPrimaryNode).Methods("POST", "OPTIONS") // Cluster endpoints api.HandleFunc("/cluster/members", ms.getClusterMembers).Methods("GET") api.HandleFunc("/cluster/refresh", ms.refreshCluster).Methods("POST", "OPTIONS") api.HandleFunc("/cluster/node/versions", ms.getClusterNodeVersions).Methods("GET") api.HandleFunc("/rollout", ms.startRollout).Methods("POST", "OPTIONS") // Task endpoints api.HandleFunc("/tasks/status", ms.getTaskStatus).Methods("GET") // Node endpoints api.HandleFunc("/node/status", ms.getNodeStatus).Methods("GET") api.HandleFunc("/node/status/{ip}", ms.getNodeStatusByIP).Methods("GET") api.HandleFunc("/node/endpoints", ms.getNodeEndpoints).Methods("GET") api.HandleFunc("/node/update", ms.updateNodeFirmware).Methods("POST", "OPTIONS") // Proxy endpoints api.HandleFunc("/proxy-call", ms.proxyCall).Methods("POST", "OPTIONS") // Registry proxy endpoints api.HandleFunc("/registry/health", ms.getRegistryHealth).Methods("GET") api.HandleFunc("/registry/firmware", ms.listRegistryFirmware).Methods("GET") api.HandleFunc("/registry/firmware", ms.uploadRegistryFirmware).Methods("POST", "OPTIONS") api.HandleFunc("/registry/firmware/{name}/{version}", ms.downloadRegistryFirmware).Methods("GET") api.HandleFunc("/registry/firmware/{name}/{version}", ms.updateRegistryFirmware).Methods("PUT", "OPTIONS") api.HandleFunc("/registry/firmware/{name}/{version}", ms.deleteRegistryFirmware).Methods("DELETE", "OPTIONS") // Monitoring endpoints api.HandleFunc("/monitoring/resources", ms.getMonitoringResources).Methods("GET") // Test endpoints api.HandleFunc("/test/websocket", ms.testWebSocket).Methods("POST", "OPTIONS") // Health check api.HandleFunc("/health", ms.healthCheck).Methods("GET") // WebSocket endpoint if ms.enableWS { ms.router.HandleFunc("/ws", ms.corsMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if err := ms.wsServer.HandleWebSocket(w, r); err != nil { log.WithError(err).Error("WebSocket connection failed") http.Error(w, "WebSocket upgrade failed", http.StatusBadRequest) } })).ServeHTTP) } } // Start starts the HTTP server func (ms *MockHTTPServer) Start() error { log.WithField("port", ms.port).Info("Starting mock HTTP server") return ms.server.ListenAndServe() } // Shutdown gracefully shuts down the HTTP server func (ms *MockHTTPServer) Shutdown(ctx context.Context) error { log.Info("Shutting down mock HTTP server") // Shutdown WebSocket server if enabled if ms.enableWS && ms.wsServer != nil { if err := ms.wsServer.Shutdown(ctx); err != nil { log.WithError(err).Error("WebSocket server shutdown error") } } return ms.server.Shutdown(ctx) } // API endpoint handlers // GET /api/discovery/nodes func (ms *MockHTTPServer) getDiscoveryNodes(w http.ResponseWriter, r *http.Request) { nodes := ms.discovery.GetNodes() primaryNode := ms.discovery.GetPrimaryNode() clusterStatus := ms.discovery.GetClusterStatus() type NodeResponse struct { *discovery.NodeInfo IsPrimary bool `json:"isPrimary"` } response := struct { PrimaryNode string `json:"primaryNode"` TotalNodes int `json:"totalNodes"` Nodes []NodeResponse `json:"nodes"` ClientInitialized bool `json:"clientInitialized"` ClientBaseURL string `json:"clientBaseUrl"` ClusterStatus discovery.ClusterStatus `json:"clusterStatus"` }{ PrimaryNode: primaryNode, TotalNodes: len(nodes), Nodes: make([]NodeResponse, 0, len(nodes)), ClientInitialized: primaryNode != "", ClientBaseURL: fmt.Sprintf("http://%s", primaryNode), ClusterStatus: clusterStatus, } for _, node := range nodes { nodeResponse := NodeResponse{ NodeInfo: node, IsPrimary: node.IP == primaryNode, } response.Nodes = append(response.Nodes, nodeResponse) } json.NewEncoder(w).Encode(response) } // POST /api/discovery/refresh func (ms *MockHTTPServer) refreshDiscovery(w http.ResponseWriter, r *http.Request) { response := struct { Success bool `json:"success"` Message string `json:"message"` PrimaryNode string `json:"primaryNode"` TotalNodes int `json:"totalNodes"` ClientInitialized bool `json:"clientInitialized"` }{ Success: true, Message: "Mock cluster refresh completed", PrimaryNode: ms.discovery.GetPrimaryNode(), TotalNodes: len(ms.discovery.GetNodes()), ClientInitialized: ms.discovery.GetPrimaryNode() != "", } json.NewEncoder(w).Encode(response) } // POST /api/discovery/random-primary func (ms *MockHTTPServer) selectRandomPrimary(w http.ResponseWriter, r *http.Request) { nodes := ms.discovery.GetNodes() if len(nodes) == 0 { http.Error(w, `{"error": "No nodes available"}`, http.StatusNotFound) return } newPrimary := ms.discovery.SelectRandomPrimaryNode() if newPrimary == "" { http.Error(w, `{"error": "Selection failed"}`, http.StatusInternalServerError) return } response := struct { Success bool `json:"success"` Message string `json:"message"` PrimaryNode string `json:"primaryNode"` TotalNodes int `json:"totalNodes"` ClientInitialized bool `json:"clientInitialized"` Timestamp string `json:"timestamp"` }{ Success: true, Message: fmt.Sprintf("Randomly selected new primary node: %s", newPrimary), PrimaryNode: newPrimary, TotalNodes: len(nodes), ClientInitialized: true, Timestamp: time.Now().Format(time.RFC3339), } json.NewEncoder(w).Encode(response) } // POST /api/discovery/primary/{ip} func (ms *MockHTTPServer) setPrimaryNode(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) requestedIP := vars["ip"] if err := ms.discovery.SetPrimaryNode(requestedIP); err != nil { http.Error(w, fmt.Sprintf(`{"error": "Node not found", "message": "Node with IP %s not found"}`, requestedIP), http.StatusNotFound) return } response := struct { Success bool `json:"success"` Message string `json:"message"` PrimaryNode string `json:"primaryNode"` ClientInitialized bool `json:"clientInitialized"` }{ Success: true, Message: fmt.Sprintf("Primary node set to %s", requestedIP), PrimaryNode: requestedIP, ClientInitialized: true, } json.NewEncoder(w).Encode(response) } // GET /api/cluster/members func (ms *MockHTTPServer) getClusterMembers(w http.ResponseWriter, r *http.Request) { nodes := ms.discovery.GetNodes() // Convert to mock format mockNodes := make(map[string]*NodeInfo) for ip, node := range nodes { mockNodes[ip] = &NodeInfo{ IP: node.IP, Hostname: node.Hostname, Status: string(node.Status), Latency: node.Latency, LastSeen: node.LastSeen, Labels: node.Labels, } } members := GenerateMockClusterMembers(mockNodes) response := &client.ClusterStatusResponse{ Members: members, } json.NewEncoder(w).Encode(response) } // POST /api/cluster/refresh func (ms *MockHTTPServer) refreshCluster(w http.ResponseWriter, r *http.Request) { var requestBody struct { Reason string `json:"reason"` } if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil && err.Error() != "EOF" { requestBody.Reason = "manual_refresh" } response := struct { Success bool `json:"success"` Message string `json:"message"` Reason string `json:"reason"` WSclients int `json:"wsClients"` }{ Success: true, Message: "Mock cluster refresh triggered", Reason: requestBody.Reason, } if ms.enableWS && ms.wsServer != nil { response.WSclients = ms.wsServer.GetClientCount() } json.NewEncoder(w).Encode(response) } // GET /api/cluster/node/versions func (ms *MockHTTPServer) getClusterNodeVersions(w http.ResponseWriter, r *http.Request) { nodes := ms.discovery.GetNodes() type NodeVersionInfo struct { IP string `json:"ip"` Version string `json:"version"` Labels map[string]string `json:"labels"` } var nodeVersions []NodeVersionInfo for _, node := range nodes { version := "unknown" if v, exists := node.Labels["version"]; exists { version = v } nodeVersions = append(nodeVersions, NodeVersionInfo{ IP: node.IP, Version: version, Labels: node.Labels, }) } response := struct { Nodes []NodeVersionInfo `json:"nodes"` }{ Nodes: nodeVersions, } json.NewEncoder(w).Encode(response) } // RolloutNode represents a node in a rollout request type RolloutNode struct { IP string `json:"ip"` Version string `json:"version"` Labels map[string]string `json:"labels"` } // POST /api/rollout func (ms *MockHTTPServer) startRollout(w http.ResponseWriter, r *http.Request) { var request struct { Firmware struct { Name string `json:"name"` Version string `json:"version"` Labels map[string]string `json:"labels"` } `json:"firmware"` Nodes []RolloutNode `json:"nodes"` } if err := json.NewDecoder(r.Body).Decode(&request); err != nil { http.Error(w, `{"error": "Invalid JSON"}`, http.StatusBadRequest) return } rolloutID := fmt.Sprintf("rollout_%d", time.Now().Unix()) response := struct { Success bool `json:"success"` Message string `json:"message"` RolloutID string `json:"rolloutId"` TotalNodes int `json:"totalNodes"` FirmwareURL string `json:"firmwareUrl"` }{ Success: true, Message: fmt.Sprintf("Mock rollout started for %d nodes", len(request.Nodes)), RolloutID: rolloutID, TotalNodes: len(request.Nodes), FirmwareURL: fmt.Sprintf("http://localhost:3002/firmware/%s/%s", request.Firmware.Name, request.Firmware.Version), } json.NewEncoder(w).Encode(response) // Simulate rollout progress in background if ms.enableWS && ms.wsServer != nil { go ms.simulateRollout(rolloutID, request.Nodes, request.Firmware.Version) } } // simulateRollout simulates a rollout process with progress updates func (ms *MockHTTPServer) simulateRollout(rolloutID string, nodes []RolloutNode, newVersion string) { for i, node := range nodes { // Simulate updating labels time.Sleep(500 * time.Millisecond) if ms.wsServer != nil { ms.wsServer.BroadcastRolloutProgress(rolloutID, node.IP, "updating_labels", i+1, len(nodes)) } // Simulate uploading time.Sleep(1 * time.Second) if ms.wsServer != nil { ms.wsServer.BroadcastRolloutProgress(rolloutID, node.IP, "uploading", i+1, len(nodes)) } // Update node version in discovery ms.discovery.UpdateNodeVersion(node.IP, strings.TrimPrefix(newVersion, "v")) // Simulate completion time.Sleep(500 * time.Millisecond) if ms.wsServer != nil { ms.wsServer.BroadcastRolloutProgress(rolloutID, node.IP, "completed", i+1, len(nodes)) } } log.WithField("rollout_id", rolloutID).Info("Mock rollout completed") } // GET /api/tasks/status func (ms *MockHTTPServer) getTaskStatus(w http.ResponseWriter, r *http.Request) { response := GenerateMockTaskStatus() json.NewEncoder(w).Encode(response) } // GET /api/node/status func (ms *MockHTTPServer) getNodeStatus(w http.ResponseWriter, r *http.Request) { primaryNode := ms.discovery.GetPrimaryNode() nodes := ms.discovery.GetNodes() var labels map[string]string if primaryNode != "" && nodes[primaryNode] != nil { labels = nodes[primaryNode].Labels } else { labels = make(map[string]string) } response := GenerateMockSystemStatus(labels) json.NewEncoder(w).Encode(response) } // GET /api/node/status/{ip} func (ms *MockHTTPServer) getNodeStatusByIP(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) nodeIP := vars["ip"] nodes := ms.discovery.GetNodes() node, exists := nodes[nodeIP] if !exists { http.Error(w, fmt.Sprintf(`{"error": "Node not found", "message": "Node with IP %s not found"}`, nodeIP), http.StatusNotFound) return } response := GenerateMockSystemStatus(node.Labels) json.NewEncoder(w).Encode(response) } // GET /api/node/endpoints func (ms *MockHTTPServer) getNodeEndpoints(w http.ResponseWriter, r *http.Request) { response := GenerateMockCapabilities() json.NewEncoder(w).Encode(response) } // POST /api/node/update func (ms *MockHTTPServer) updateNodeFirmware(w http.ResponseWriter, r *http.Request) { nodeIP := r.URL.Query().Get("ip") if nodeIP == "" { nodeIP = r.Header.Get("X-Node-IP") } if nodeIP == "" { http.Error(w, `{"error": "Node IP required"}`, http.StatusBadRequest) return } // Parse multipart form err := r.ParseMultipartForm(50 << 20) // 50MB limit if err != nil { http.Error(w, `{"error": "Failed to parse form"}`, http.StatusBadRequest) return } file, fileHeader, err := r.FormFile("file") if err != nil { http.Error(w, `{"error": "No file received"}`, http.StatusBadRequest) return } defer file.Close() filename := fileHeader.Filename if filename == "" { filename = "firmware.bin" } // Read file data fileData, err := io.ReadAll(file) if err != nil { http.Error(w, `{"error": "Failed to read file"}`, http.StatusInternalServerError) return } log.WithFields(log.Fields{ "node_ip": nodeIP, "file_size": len(fileData), "filename": filename, }).Info("Mock firmware upload received") // Store firmware in memory ms.firmwareStore[nodeIP] = fileData // Broadcast status if WebSocket enabled if ms.enableWS && ms.wsServer != nil { ms.wsServer.BroadcastFirmwareUploadStatus(nodeIP, "uploading", filename, len(fileData)) } // Send immediate response response := struct { Success bool `json:"success"` Message string `json:"message"` NodeIP string `json:"nodeIp"` FileSize int `json:"fileSize"` Filename string `json:"filename"` Status string `json:"status"` }{ Success: true, Message: "Mock firmware upload received", NodeIP: nodeIP, FileSize: len(fileData), Filename: filename, Status: "processing", } json.NewEncoder(w).Encode(response) // Simulate firmware update in background go func() { time.Sleep(2 * time.Second) if ms.enableWS && ms.wsServer != nil { // Randomly succeed or fail (90% success rate) if rand.Float32() < 0.9 { ms.wsServer.BroadcastFirmwareUploadStatus(nodeIP, "completed", filename, len(fileData)) } else { ms.wsServer.BroadcastFirmwareUploadStatus(nodeIP, "failed", filename, len(fileData)) } } }() } // POST /api/proxy-call func (ms *MockHTTPServer) proxyCall(w http.ResponseWriter, r *http.Request) { var requestBody struct { IP string `json:"ip"` Method string `json:"method"` URI string `json:"uri"` Params []map[string]interface{} `json:"params"` } if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil { http.Error(w, `{"error": "Invalid JSON"}`, http.StatusBadRequest) return } // Generate mock response based on URI mockResponse := GenerateMockProxyResponse(requestBody.Method, requestBody.URI) // Wrap in data field for consistency response := map[string]interface{}{ "data": mockResponse, "status": http.StatusOK, } json.NewEncoder(w).Encode(response) } // GET /api/registry/health func (ms *MockHTTPServer) getRegistryHealth(w http.ResponseWriter, r *http.Request) { response := map[string]interface{}{ "status": "healthy", "timestamp": time.Now().Format(time.RFC3339), "service": "mock-registry", } json.NewEncoder(w).Encode(response) } // GET /api/registry/firmware func (ms *MockHTTPServer) listRegistryFirmware(w http.ResponseWriter, r *http.Request) { firmwareList := GenerateMockFirmwareList() json.NewEncoder(w).Encode(firmwareList) } // POST /api/registry/firmware func (ms *MockHTTPServer) uploadRegistryFirmware(w http.ResponseWriter, r *http.Request) { response := map[string]interface{}{ "success": true, "message": "Mock firmware uploaded successfully", } json.NewEncoder(w).Encode(response) } // GET /api/registry/firmware/{name}/{version} func (ms *MockHTTPServer) downloadRegistryFirmware(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) name := vars["name"] version := vars["version"] // Generate mock firmware binary firmwareData := GenerateMockFirmwareBinary(524288) // 512KB w.Header().Set("Content-Type", "application/octet-stream") w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s-%s.bin\"", name, version)) w.Header().Set("Content-Length", fmt.Sprintf("%d", len(firmwareData))) w.Write(firmwareData) } // PUT /api/registry/firmware/{name}/{version} func (ms *MockHTTPServer) updateRegistryFirmware(w http.ResponseWriter, r *http.Request) { response := map[string]interface{}{ "success": true, "message": "Mock firmware metadata updated", } json.NewEncoder(w).Encode(response) } // DELETE /api/registry/firmware/{name}/{version} func (ms *MockHTTPServer) deleteRegistryFirmware(w http.ResponseWriter, r *http.Request) { response := map[string]interface{}{ "success": true, "message": "Mock firmware deleted", } json.NewEncoder(w).Encode(response) } // POST /api/test/websocket func (ms *MockHTTPServer) testWebSocket(w http.ResponseWriter, r *http.Request) { response := struct { Success bool `json:"success"` Message string `json:"message"` WSclients int `json:"websocketClients"` TotalNodes int `json:"totalNodes"` }{ Success: true, Message: "Mock WebSocket test broadcast sent", TotalNodes: len(ms.discovery.GetNodes()), } if ms.enableWS && ms.wsServer != nil { response.WSclients = ms.wsServer.GetClientCount() // Trigger a test broadcast ms.wsServer.BroadcastClusterUpdate() } json.NewEncoder(w).Encode(response) } // GET /api/monitoring/resources func (ms *MockHTTPServer) getMonitoringResources(w http.ResponseWriter, r *http.Request) { nodes := ms.discovery.GetNodes() // Convert to mock format mockNodes := make(map[string]*NodeInfo) for ip, node := range nodes { mockNodes[ip] = &NodeInfo{ IP: node.IP, Hostname: node.Hostname, Status: string(node.Status), Latency: node.Latency, LastSeen: node.LastSeen, Labels: node.Labels, } } response := GenerateMockMonitoringResources(mockNodes) json.NewEncoder(w).Encode(response) } // GET /api/health func (ms *MockHTTPServer) healthCheck(w http.ResponseWriter, r *http.Request) { nodes := ms.discovery.GetNodes() primaryNode := ms.discovery.GetPrimaryNode() health := struct { Status string `json:"status"` Timestamp string `json:"timestamp"` Services map[string]bool `json:"services"` Cluster map[string]interface{} `json:"cluster"` Mock bool `json:"mock"` }{ Status: "healthy", Timestamp: time.Now().Format(time.RFC3339), Services: map[string]bool{ "http": true, "websocket": ms.enableWS, "discovery": true, "mockClient": primaryNode != "", }, Cluster: map[string]interface{}{ "totalNodes": len(nodes), "primaryNode": primaryNode, }, Mock: true, } json.NewEncoder(w).Encode(health) }