package handler import ( "encoding/json" "fmt" "net/http" "net/http/httptest" "testing" "gcy_hpc_server/internal/service" "gcy_hpc_server/internal/slurm" "github.com/gin-gonic/gin" "go.uber.org/zap" "go.uber.org/zap/zapcore" "go.uber.org/zap/zaptest/observer" ) func setupClusterHandler(slurmHandler http.HandlerFunc) (*httptest.Server, *ClusterHandler) { srv := httptest.NewServer(slurmHandler) client, _ := slurm.NewClient(srv.URL, srv.Client()) clusterSvc := service.NewClusterService(client, zap.NewNop()) return srv, NewClusterHandler(clusterSvc, zap.NewNop()) } func setupClusterHandlerWithObserver(slurmHandler http.HandlerFunc) (*httptest.Server, *ClusterHandler, *observer.ObservedLogs) { core, recorded := observer.New(zapcore.DebugLevel) l := zap.New(core) srv := httptest.NewServer(slurmHandler) client, _ := slurm.NewClient(srv.URL, srv.Client()) clusterSvc := service.NewClusterService(client, l) return srv, NewClusterHandler(clusterSvc, l), recorded } func setupClusterRouter(h *ClusterHandler) *gin.Engine { gin.SetMode(gin.TestMode) r := gin.New() v1 := r.Group("/api/v1") v1.GET("/nodes", h.GetNodes) v1.GET("/nodes/:name", h.GetNode) v1.GET("/partitions", h.GetPartitions) v1.GET("/partitions/:name", h.GetPartition) v1.GET("/diag", h.GetDiag) return r } func TestGetNodes_Success(t *testing.T) { srv, h := setupClusterHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "nodes": []map[string]interface{}{ {"name": "node1", "state": []string{"IDLE"}, "cpus": 64, "real_memory": 128000}, }, "last_update": map[string]interface{}{}, }) })) defer srv.Close() router := setupClusterRouter(h) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/nodes", nil) router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d", w.Code) } var resp map[string]interface{} if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("failed to parse response: %v", err) } if resp["success"] != true { t.Fatal("expected success=true") } } func TestGetNode_Success(t *testing.T) { srv, h := setupClusterHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "nodes": []map[string]interface{}{ {"name": "node1", "state": []string{"IDLE"}, "cpus": 64, "real_memory": 128000}, }, "last_update": map[string]interface{}{}, }) })) defer srv.Close() router := setupClusterRouter(h) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/nodes/node1", nil) router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d", w.Code) } var resp map[string]interface{} if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("failed to parse response: %v", err) } if resp["success"] != true { t.Fatal("expected success=true") } } func TestGetNode_NotFound(t *testing.T) { srv, h := setupClusterHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "nodes": []map[string]interface{}{}, "last_update": map[string]interface{}{}, }) })) defer srv.Close() router := setupClusterRouter(h) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/nodes/nonexistent", nil) router.ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Fatalf("expected 404, got %d", w.Code) } } func TestGetPartitions_Success(t *testing.T) { srv, h := setupClusterHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "partitions": []map[string]interface{}{ { "name": "normal", "partition": map[string]interface{}{ "state": []string{"UP"}, }, "nodes": map[string]interface{}{ "configured": "node[1-10]", "total": int32(10), }, "cpus": map[string]interface{}{ "total": int32(640), }, "maximums": map[string]interface{}{ "time": map[string]interface{}{"number": int64(60)}, }, }, }, "last_update": map[string]interface{}{}, }) })) defer srv.Close() router := setupClusterRouter(h) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/partitions", nil) router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d", w.Code) } var resp map[string]interface{} if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("failed to parse response: %v", err) } if resp["success"] != true { t.Fatal("expected success=true") } } func TestGetPartition_Success(t *testing.T) { srv, h := setupClusterHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "partitions": []map[string]interface{}{ { "name": "normal", "partition": map[string]interface{}{ "state": []string{"UP"}, }, "nodes": map[string]interface{}{ "configured": "node[1-10]", "total": int32(10), }, "cpus": map[string]interface{}{ "total": int32(640), }, "maximums": map[string]interface{}{ "time": map[string]interface{}{"number": int64(60)}, }, }, }, "last_update": map[string]interface{}{}, }) })) defer srv.Close() router := setupClusterRouter(h) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/partitions/normal", nil) router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d", w.Code) } var resp map[string]interface{} if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("failed to parse response: %v", err) } if resp["success"] != true { t.Fatal("expected success=true") } } func TestGetPartition_NotFound(t *testing.T) { srv, h := setupClusterHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "partitions": []map[string]interface{}{}, "last_update": map[string]interface{}{}, }) })) defer srv.Close() router := setupClusterRouter(h) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/partitions/nonexistent", nil) router.ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Fatalf("expected 404, got %d", w.Code) } } func TestGetDiag_Success(t *testing.T) { srv, h := setupClusterHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "statistics": map[string]interface{}{ "server_thread_count": 3, "agent_queue_size": 0, "jobs_submitted": 100, "jobs_started": 90, "jobs_completed": 85, "schedule_cycle_last": 10, "schedule_cycle_total": 500, }, }) })) defer srv.Close() router := setupClusterRouter(h) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/diag", nil) router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d", w.Code) } var resp map[string]interface{} if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("failed to parse response: %v", err) } if resp["success"] != true { t.Fatal("expected success=true") } } // --- Logging tests --- func TestClusterHandler_GetNodes_InternalError_LogsError(t *testing.T) { srv, h, recorded := setupClusterHandlerWithObserver(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) fmt.Fprint(w, `{"errors":[{"error":"internal"}]}`) })) defer srv.Close() router := setupClusterRouter(h) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/nodes", nil) router.ServeHTTP(w, req) if w.Code != http.StatusInternalServerError { t.Fatalf("expected 500, got %d", w.Code) } handlerLogs := recorded.FilterMessage("handler error") if handlerLogs.Len() != 1 { t.Fatalf("expected 1 handler error log, got %d", handlerLogs.Len()) } entry := handlerLogs.All()[0] if entry.Level != zapcore.ErrorLevel { t.Fatalf("expected Error level, got %v", entry.Level) } assertField(t, entry.Context, "method", "GetNodes") } func TestClusterHandler_GetNodes_Success_NoLogs(t *testing.T) { srv, h, recorded := setupClusterHandlerWithObserver(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "nodes": []map[string]interface{}{ {"name": "node1"}, }, "last_update": map[string]interface{}{}, }) })) defer srv.Close() router := setupClusterRouter(h) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/nodes", nil) router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d", w.Code) } if recorded.Len() != 0 { t.Fatalf("expected 0 log entries on success, got %d", recorded.Len()) } } func TestClusterHandler_GetNode_InternalError_LogsError(t *testing.T) { srv, h, recorded := setupClusterHandlerWithObserver(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) fmt.Fprint(w, `{"errors":[{"error":"internal"}]}`) })) defer srv.Close() router := setupClusterRouter(h) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/nodes/node1", nil) router.ServeHTTP(w, req) if w.Code != http.StatusInternalServerError { t.Fatalf("expected 500, got %d", w.Code) } handlerLogs := recorded.FilterMessage("handler error") if handlerLogs.Len() != 1 { t.Fatalf("expected 1 handler error log, got %d", handlerLogs.Len()) } entry := handlerLogs.All()[0] if entry.Level != zapcore.ErrorLevel { t.Fatalf("expected Error level, got %v", entry.Level) } assertField(t, entry.Context, "method", "GetNode") } func TestClusterHandler_GetNode_NotFound_LogsWarn(t *testing.T) { srv, h, recorded := setupClusterHandlerWithObserver(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "nodes": []map[string]interface{}{}, "last_update": map[string]interface{}{}, }) })) defer srv.Close() router := setupClusterRouter(h) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/nodes/nonexistent", nil) router.ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Fatalf("expected 404, got %d", w.Code) } if recorded.Len() != 1 { t.Fatalf("expected 1 log entry, got %d", recorded.Len()) } entry := recorded.All()[0] if entry.Level != zapcore.WarnLevel { t.Fatalf("expected Warn level, got %v", entry.Level) } if entry.Message != "not found" { t.Fatalf("expected message 'not found', got %q", entry.Message) } assertField(t, entry.Context, "method", "GetNode") assertField(t, entry.Context, "name", "nonexistent") } func TestClusterHandler_GetNode_Success_NoLogs(t *testing.T) { srv, h, recorded := setupClusterHandlerWithObserver(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "nodes": []map[string]interface{}{ {"name": "node1", "state": []string{"IDLE"}, "cpus": 64, "real_memory": 128000}, }, "last_update": map[string]interface{}{}, }) })) defer srv.Close() router := setupClusterRouter(h) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/nodes/node1", nil) router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d", w.Code) } if recorded.Len() != 0 { t.Fatalf("expected 0 log entries on success, got %d", recorded.Len()) } } func TestClusterHandler_GetPartitions_InternalError_LogsError(t *testing.T) { srv, h, recorded := setupClusterHandlerWithObserver(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) fmt.Fprint(w, `{"errors":[{"error":"internal"}]}`) })) defer srv.Close() router := setupClusterRouter(h) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/partitions", nil) router.ServeHTTP(w, req) if w.Code != http.StatusInternalServerError { t.Fatalf("expected 500, got %d", w.Code) } handlerLogs := recorded.FilterMessage("handler error") if handlerLogs.Len() != 1 { t.Fatalf("expected 1 handler error log, got %d", handlerLogs.Len()) } entry := handlerLogs.All()[0] if entry.Level != zapcore.ErrorLevel { t.Fatalf("expected Error level, got %v", entry.Level) } assertField(t, entry.Context, "method", "GetPartitions") } func TestClusterHandler_GetPartitions_Success_NoLogs(t *testing.T) { srv, h, recorded := setupClusterHandlerWithObserver(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "partitions": []map[string]interface{}{ {"name": "normal"}, }, "last_update": map[string]interface{}{}, }) })) defer srv.Close() router := setupClusterRouter(h) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/partitions", nil) router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d", w.Code) } if recorded.Len() != 0 { t.Fatalf("expected 0 log entries on success, got %d", recorded.Len()) } } func TestClusterHandler_GetPartition_InternalError_LogsError(t *testing.T) { srv, h, recorded := setupClusterHandlerWithObserver(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) fmt.Fprint(w, `{"errors":[{"error":"internal"}]}`) })) defer srv.Close() router := setupClusterRouter(h) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/partitions/normal", nil) router.ServeHTTP(w, req) if w.Code != http.StatusInternalServerError { t.Fatalf("expected 500, got %d", w.Code) } handlerLogs := recorded.FilterMessage("handler error") if handlerLogs.Len() != 1 { t.Fatalf("expected 1 handler error log, got %d", handlerLogs.Len()) } entry := handlerLogs.All()[0] if entry.Level != zapcore.ErrorLevel { t.Fatalf("expected Error level, got %v", entry.Level) } assertField(t, entry.Context, "method", "GetPartition") } func TestClusterHandler_GetPartition_NotFound_LogsWarn(t *testing.T) { srv, h, recorded := setupClusterHandlerWithObserver(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "partitions": []map[string]interface{}{}, "last_update": map[string]interface{}{}, }) })) defer srv.Close() router := setupClusterRouter(h) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/partitions/nonexistent", nil) router.ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Fatalf("expected 404, got %d", w.Code) } if recorded.Len() != 1 { t.Fatalf("expected 1 log entry, got %d", recorded.Len()) } entry := recorded.All()[0] if entry.Level != zapcore.WarnLevel { t.Fatalf("expected Warn level, got %v", entry.Level) } if entry.Message != "not found" { t.Fatalf("expected message 'not found', got %q", entry.Message) } assertField(t, entry.Context, "method", "GetPartition") assertField(t, entry.Context, "name", "nonexistent") } func TestClusterHandler_GetPartition_Success_NoLogs(t *testing.T) { srv, h, recorded := setupClusterHandlerWithObserver(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "partitions": []map[string]interface{}{ { "name": "normal", "partition": map[string]interface{}{ "state": []string{"UP"}, }, "nodes": map[string]interface{}{ "configured": "node[1-10]", "total": int32(10), }, "cpus": map[string]interface{}{ "total": int32(640), }, "maximums": map[string]interface{}{ "time": map[string]interface{}{"number": int64(60)}, }, }, }, "last_update": map[string]interface{}{}, }) })) defer srv.Close() router := setupClusterRouter(h) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/partitions/normal", nil) router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d", w.Code) } if recorded.Len() != 0 { t.Fatalf("expected 0 log entries on success, got %d", recorded.Len()) } } func TestClusterHandler_GetDiag_InternalError_LogsError(t *testing.T) { srv, h, recorded := setupClusterHandlerWithObserver(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) fmt.Fprint(w, `{"errors":[{"error":"internal"}]}`) })) defer srv.Close() router := setupClusterRouter(h) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/diag", nil) router.ServeHTTP(w, req) if w.Code != http.StatusInternalServerError { t.Fatalf("expected 500, got %d", w.Code) } handlerLogs := recorded.FilterMessage("handler error") if handlerLogs.Len() != 1 { t.Fatalf("expected 1 handler error log, got %d", handlerLogs.Len()) } entry := handlerLogs.All()[0] if entry.Level != zapcore.ErrorLevel { t.Fatalf("expected Error level, got %v", entry.Level) } assertField(t, entry.Context, "method", "GetDiag") } func TestClusterHandler_GetDiag_Success_NoLogs(t *testing.T) { srv, h, recorded := setupClusterHandlerWithObserver(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "statistics": map[string]interface{}{ "server_thread_count": 3, "agent_queue_size": 0, "jobs_submitted": 100, "jobs_started": 90, "jobs_completed": 85, "schedule_cycle_last": 10, "schedule_cycle_total": 500, }, }) })) defer srv.Close() router := setupClusterRouter(h) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/diag", nil) router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d", w.Code) } if recorded.Len() != 0 { t.Fatalf("expected 0 log entries on success, got %d", recorded.Len()) } } // assertField checks that a zap Field slice contains a string field with the given key and value. func assertField(t *testing.T, fields []zapcore.Field, key, value string) { t.Helper() for _, f := range fields { if f.Key == key && f.String == value { return } } t.Fatalf("expected field %q=%q in context, got %v", key, value, fields) }