@@ -331,6 +331,263 @@ func (s *NodesSuite) TestNodesStatsSummaryDenied() {
331331	})
332332}
333333
334+ func  (s  * NodesSuite ) TestNodesTop () {
335+ 	s .mockServer .Handle (http .HandlerFunc (func (w  http.ResponseWriter , req  * http.Request ) {
336+ 		w .Header ().Set ("Content-Type" , "application/json" )
337+ 		// Request Performed by DiscoveryClient to Kube API (Get API Groups legacy -core-) 
338+ 		if  req .URL .Path  ==  "/api"  {
339+ 			_ , _  =  w .Write ([]byte (`{"kind":"APIVersions","versions":["v1"],"serverAddressByClientCIDRs":[{"clientCIDR":"0.0.0.0/0"}]}` ))
340+ 			return 
341+ 		}
342+ 		// Request Performed by DiscoveryClient to Kube API (Get API Groups) 
343+ 		if  req .URL .Path  ==  "/apis"  {
344+ 			_ , _  =  w .Write ([]byte (`{"kind":"APIGroupList","apiVersion":"v1","groups":[{"name":"metrics.k8s.io","versions":[{"groupVersion":"metrics.k8s.io/v1beta1","version":"v1beta1"}],"preferredVersion":{"groupVersion":"metrics.k8s.io/v1beta1","version":"v1beta1"}}]}` ))
345+ 			return 
346+ 		}
347+ 		// Request Performed by DiscoveryClient to Kube API (Get API Resources) 
348+ 		if  req .URL .Path  ==  "/apis/metrics.k8s.io/v1beta1"  {
349+ 			_ , _  =  w .Write ([]byte (`{"kind":"APIResourceList","apiVersion":"v1","groupVersion":"metrics.k8s.io/v1beta1","resources":[{"name":"nodes","singularName":"","namespaced":false,"kind":"NodeMetrics","verbs":["get","list"]}]}` ))
350+ 			return 
351+ 		}
352+ 		// List Nodes 
353+ 		if  req .URL .Path  ==  "/api/v1/nodes"  {
354+ 			_ , _  =  w .Write ([]byte (`{ 
355+ 				"apiVersion": "v1", 
356+ 				"kind": "NodeList", 
357+ 				"items": [ 
358+ 					{ 
359+ 						"metadata": { 
360+ 							"name": "node-1", 
361+ 							"labels": { 
362+ 								"node-role.kubernetes.io/worker": "" 
363+ 							} 
364+ 						}, 
365+ 						"status": { 
366+ 							"allocatable": { 
367+ 								"cpu": "4", 
368+ 								"memory": "16Gi" 
369+ 							}, 
370+ 							"nodeInfo": { 
371+ 								"swap": { 
372+ 									"capacity": 0 
373+ 								} 
374+ 							} 
375+ 						} 
376+ 					}, 
377+ 					{ 
378+ 						"metadata": { 
379+ 							"name": "node-2", 
380+ 							"labels": { 
381+ 								"node-role.kubernetes.io/worker": "" 
382+ 							} 
383+ 						}, 
384+ 						"status": { 
385+ 							"allocatable": { 
386+ 								"cpu": "4", 
387+ 								"memory": "16Gi" 
388+ 							}, 
389+ 							"nodeInfo": { 
390+ 								"swap": { 
391+ 									"capacity": 0 
392+ 								} 
393+ 							} 
394+ 						} 
395+ 					} 
396+ 				] 
397+ 			}` ))
398+ 			return 
399+ 		}
400+ 		// Get NodeMetrics 
401+ 		if  req .URL .Path  ==  "/apis/metrics.k8s.io/v1beta1/nodes"  {
402+ 			_ , _  =  w .Write ([]byte (`{ 
403+ 				"apiVersion": "metrics.k8s.io/v1beta1", 
404+ 				"kind": "NodeMetricsList", 
405+ 				"items": [ 
406+ 					{ 
407+ 						"metadata": { 
408+ 							"name": "node-1" 
409+ 						}, 
410+ 						"timestamp": "2025-10-29T09:00:00Z", 
411+ 						"window": "30s", 
412+ 						"usage": { 
413+ 							"cpu": "500m", 
414+ 							"memory": "2Gi" 
415+ 						} 
416+ 					}, 
417+ 					{ 
418+ 						"metadata": { 
419+ 							"name": "node-2" 
420+ 						}, 
421+ 						"timestamp": "2025-10-29T09:00:00Z", 
422+ 						"window": "30s", 
423+ 						"usage": { 
424+ 							"cpu": "1000m", 
425+ 							"memory": "4Gi" 
426+ 						} 
427+ 					} 
428+ 				] 
429+ 			}` ))
430+ 			return 
431+ 		}
432+ 		// Get specific NodeMetrics 
433+ 		if  req .URL .Path  ==  "/apis/metrics.k8s.io/v1beta1/nodes/node-1"  {
434+ 			_ , _  =  w .Write ([]byte (`{ 
435+ 				"apiVersion": "metrics.k8s.io/v1beta1", 
436+ 				"kind": "NodeMetrics", 
437+ 				"metadata": { 
438+ 					"name": "node-1" 
439+ 				}, 
440+ 				"timestamp": "2025-10-29T09:00:00Z", 
441+ 				"window": "30s", 
442+ 				"usage": { 
443+ 					"cpu": "500m", 
444+ 					"memory": "2Gi" 
445+ 				} 
446+ 			}` ))
447+ 			return 
448+ 		}
449+ 		w .WriteHeader (http .StatusNotFound )
450+ 	}))
451+ 	s .InitMcpClient ()
452+ 
453+ 	s .Run ("nodes_top() - all nodes" , func () {
454+ 		toolResult , err  :=  s .CallTool ("nodes_top" , map [string ]interface {}{})
455+ 		s .Require ().NotNil (toolResult , "toolResult should not be nil" )
456+ 		s .Run ("no error" , func () {
457+ 			s .Falsef (toolResult .IsError , "call tool should succeed" )
458+ 			s .Nilf (err , "call tool should not return error object" )
459+ 		})
460+ 		s .Run ("returns metrics for all nodes" , func () {
461+ 			content  :=  toolResult .Content [0 ].(mcp.TextContent ).Text 
462+ 			s .Contains (content , "node-1" , "expected metrics to contain node-1" )
463+ 			s .Contains (content , "node-2" , "expected metrics to contain node-2" )
464+ 			s .Contains (content , "CPU(cores)" , "expected header with CPU column" )
465+ 			s .Contains (content , "MEMORY(bytes)" , "expected header with MEMORY column" )
466+ 		})
467+ 	})
468+ 
469+ 	s .Run ("nodes_top(name=node-1) - specific node" , func () {
470+ 		toolResult , err  :=  s .CallTool ("nodes_top" , map [string ]interface {}{
471+ 			"name" : "node-1" ,
472+ 		})
473+ 		s .Require ().NotNil (toolResult , "toolResult should not be nil" )
474+ 		s .Run ("no error" , func () {
475+ 			s .Falsef (toolResult .IsError , "call tool should succeed" )
476+ 			s .Nilf (err , "call tool should not return error object" )
477+ 		})
478+ 		s .Run ("returns metrics for specific node" , func () {
479+ 			content  :=  toolResult .Content [0 ].(mcp.TextContent ).Text 
480+ 			s .Contains (content , "node-1" , "expected metrics to contain node-1" )
481+ 			s .Contains (content , "500m" , "expected CPU usage of 500m" )
482+ 			s .Contains (content , "2048Mi" , "expected memory usage of 2048Mi" )
483+ 		})
484+ 	})
485+ 
486+ 	s .Run ("nodes_top(label_selector=node-role.kubernetes.io/worker=)" , func () {
487+ 		toolResult , err  :=  s .CallTool ("nodes_top" , map [string ]interface {}{
488+ 			"label_selector" : "node-role.kubernetes.io/worker=" ,
489+ 		})
490+ 		s .Require ().NotNil (toolResult , "toolResult should not be nil" )
491+ 		s .Run ("no error" , func () {
492+ 			s .Falsef (toolResult .IsError , "call tool should succeed" )
493+ 			s .Nilf (err , "call tool should not return error object" )
494+ 		})
495+ 		s .Run ("returns metrics for filtered nodes" , func () {
496+ 			content  :=  toolResult .Content [0 ].(mcp.TextContent ).Text 
497+ 			s .Contains (content , "node-1" , "expected metrics to contain node-1" )
498+ 			s .Contains (content , "node-2" , "expected metrics to contain node-2" )
499+ 		})
500+ 	})
501+ }
502+ 
503+ func  (s  * NodesSuite ) TestNodesTopMetricsUnavailable () {
504+ 	s .mockServer .Handle (http .HandlerFunc (func (w  http.ResponseWriter , req  * http.Request ) {
505+ 		// List Nodes 
506+ 		if  req .URL .Path  ==  "/api/v1/nodes"  {
507+ 			w .Header ().Set ("Content-Type" , "application/json" )
508+ 			w .WriteHeader (http .StatusOK )
509+ 			_ , _  =  w .Write ([]byte (`{ 
510+ 				"apiVersion": "v1", 
511+ 				"kind": "NodeList", 
512+ 				"items": [ 
513+ 					{ 
514+ 						"metadata": { 
515+ 							"name": "node-1" 
516+ 						}, 
517+ 						"status": { 
518+ 							"allocatable": { 
519+ 								"cpu": "4", 
520+ 								"memory": "16Gi" 
521+ 							} 
522+ 						} 
523+ 					} 
524+ 				] 
525+ 			}` ))
526+ 			return 
527+ 		}
528+ 		// Metrics server not available 
529+ 		if  req .URL .Path  ==  "/apis/metrics.k8s.io/v1beta1/nodes"  {
530+ 			w .WriteHeader (http .StatusNotFound )
531+ 			return 
532+ 		}
533+ 		w .WriteHeader (http .StatusNotFound )
534+ 	}))
535+ 	s .InitMcpClient ()
536+ 
537+ 	s .Run ("nodes_top() - metrics unavailable" , func () {
538+ 		toolResult , err  :=  s .CallTool ("nodes_top" , map [string ]interface {}{})
539+ 		s .Require ().NotNil (toolResult , "toolResult should not be nil" )
540+ 		s .Run ("has error" , func () {
541+ 			s .Truef (toolResult .IsError , "call tool should fail when metrics unavailable" )
542+ 			s .Nilf (err , "call tool should not return error object" )
543+ 		})
544+ 		s .Run ("describes metrics unavailable" , func () {
545+ 			content  :=  toolResult .Content [0 ].(mcp.TextContent ).Text 
546+ 			s .Contains (content , "failed to get nodes top" , "expected error message about failing to get nodes top" )
547+ 		})
548+ 	})
549+ }
550+ 
551+ func  (s  * NodesSuite ) TestNodesTopDenied () {
552+ 	s .Require ().NoError (toml .Unmarshal ([]byte (` 
553+ 		denied_resources = [ { group = "metrics.k8s.io", version = "v1beta1" } ] 
554+ 	` ), s .Cfg ), "Expected to parse denied resources config" )
555+ 	s .mockServer .Handle (http .HandlerFunc (func (w  http.ResponseWriter , req  * http.Request ) {
556+ 		w .Header ().Set ("Content-Type" , "application/json" )
557+ 		// Request Performed by DiscoveryClient to Kube API (Get API Groups legacy -core-) 
558+ 		if  req .URL .Path  ==  "/api"  {
559+ 			_ , _  =  w .Write ([]byte (`{"kind":"APIVersions","versions":["v1"],"serverAddressByClientCIDRs":[{"clientCIDR":"0.0.0.0/0"}]}` ))
560+ 			return 
561+ 		}
562+ 		// Request Performed by DiscoveryClient to Kube API (Get API Groups) 
563+ 		if  req .URL .Path  ==  "/apis"  {
564+ 			_ , _  =  w .Write ([]byte (`{"kind":"APIGroupList","apiVersion":"v1","groups":[{"name":"metrics.k8s.io","versions":[{"groupVersion":"metrics.k8s.io/v1beta1","version":"v1beta1"}],"preferredVersion":{"groupVersion":"metrics.k8s.io/v1beta1","version":"v1beta1"}}]}` ))
565+ 			return 
566+ 		}
567+ 		// Request Performed by DiscoveryClient to Kube API (Get API Resources) 
568+ 		if  req .URL .Path  ==  "/apis/metrics.k8s.io/v1beta1"  {
569+ 			_ , _  =  w .Write ([]byte (`{"kind":"APIResourceList","apiVersion":"v1","groupVersion":"metrics.k8s.io/v1beta1","resources":[{"name":"nodes","singularName":"","namespaced":false,"kind":"NodeMetrics","verbs":["get","list"]}]}` ))
570+ 			return 
571+ 		}
572+ 		w .WriteHeader (http .StatusNotFound )
573+ 	}))
574+ 	s .InitMcpClient ()
575+ 
576+ 	s .Run ("nodes_top (denied)" , func () {
577+ 		toolResult , err  :=  s .CallTool ("nodes_top" , map [string ]interface {}{})
578+ 		s .Require ().NotNil (toolResult , "toolResult should not be nil" )
579+ 		s .Run ("has error" , func () {
580+ 			s .Truef (toolResult .IsError , "call tool should fail" )
581+ 			s .Nilf (err , "call tool should not return error object" )
582+ 		})
583+ 		s .Run ("describes denial" , func () {
584+ 			expectedMessage  :=  "failed to get nodes top: resource not allowed: metrics.k8s.io/v1beta1, Kind=NodeMetrics" 
585+ 			s .Equalf (expectedMessage , toolResult .Content [0 ].(mcp.TextContent ).Text ,
586+ 				"expected descriptive error '%s', got %v" , expectedMessage , toolResult .Content [0 ].(mcp.TextContent ).Text )
587+ 		})
588+ 	})
589+ }
590+ 
334591func  TestNodes (t  * testing.T ) {
335592	suite .Run (t , new (NodesSuite ))
336593}
0 commit comments