diff --git a/internal/ui/views_cluster_test.go b/internal/ui/views_cluster_test.go index 1edf93b..ae2223a 100644 --- a/internal/ui/views_cluster_test.go +++ b/internal/ui/views_cluster_test.go @@ -64,3 +64,321 @@ func TestApp_FormatNodeRoleBadge(t *testing.T) { }) } } + +func TestApp_RenderClusterView_NoData(t *testing.T) { + app := &App{ + health: nil, + stats: nil, + } + + result := app.renderClusterView() + + if !strings.Contains(result, "Cluster Health") { + t.Error("renderClusterView() should contain 'Cluster Health' header") + } +} + +func TestApp_RenderClusterView_GreenStatus(t *testing.T) { + stats := &ClusterStats{} + stats.Indices.Count = 5 + stats.Indices.Docs.Count = 1000 + stats.Indices.Store.SizeInBytes = 1024 * 1024 * 100 + + app := &App{ + health: &ClusterHealth{ + Status: "green", + ClusterName: "test-cluster", + NumberOfNodes: 3, + NumberOfDataNodes: 2, + ActiveShards: 10, + ActivePrimaryShards: 5, + RelocatingShards: 0, + InitializingShards: 0, + UnassignedShards: 0, + }, + stats: stats, + } + + result := app.renderClusterView() + + expectedStrings := []string{ + "Cluster Health", + "GREEN", + "test-cluster", + "Nodes", + "Shards", + "Indices", + } + + for _, expected := range expectedStrings { + if !strings.Contains(result, expected) { + t.Errorf("renderClusterView() should contain %q", expected) + } + } +} + +func TestApp_RenderClusterView_YellowStatus(t *testing.T) { + app := &App{ + health: &ClusterHealth{ + Status: "yellow", + ClusterName: "test-cluster", + NumberOfNodes: 1, + NumberOfDataNodes: 1, + ActiveShards: 5, + ActivePrimaryShards: 5, + RelocatingShards: 1, + InitializingShards: 0, + UnassignedShards: 5, + }, + } + + result := app.renderClusterView() + + if !strings.Contains(result, "YELLOW") { + t.Error("renderClusterView() should show YELLOW status") + } + + if !strings.Contains(result, "Relocating") { + t.Error("renderClusterView() should show relocating shards when > 0") + } + + if !strings.Contains(result, "Unassigned") { + t.Error("renderClusterView() should show unassigned shards when > 0") + } +} + +func TestApp_RenderClusterView_RedStatus(t *testing.T) { + app := &App{ + health: &ClusterHealth{ + Status: "red", + ClusterName: "test-cluster", + NumberOfNodes: 2, + NumberOfDataNodes: 2, + ActiveShards: 3, + ActivePrimaryShards: 3, + RelocatingShards: 0, + InitializingShards: 2, + UnassignedShards: 10, + }, + } + + result := app.renderClusterView() + + if !strings.Contains(result, "RED") { + t.Error("renderClusterView() should show RED status") + } + + if !strings.Contains(result, "Initializing") { + t.Error("renderClusterView() should show initializing shards when > 0") + } +} + +func TestApp_RenderNodesView_NoNodes(t *testing.T) { + app := &App{ + nodes: []NodeInfo{}, + } + + result := app.renderNodesView() + + if !strings.Contains(result, "No nodes data available") { + t.Error("renderNodesView() with no nodes should show 'No nodes data available'") + } +} + +func TestApp_RenderNodesView_MasterNodes(t *testing.T) { + app := &App{ + nodes: []NodeInfo{ + { + Name: "master-1", + NodeRole: "m", + Master: "*", + HeapPercent: "50", + CPU: "30", + RAMPercent: "60", + DiskUsedPercent: "40", + }, + }, + } + + result := app.renderNodesView() + + expectedStrings := []string{ + "Nodes (1)", + "Node Types", + "Master/Controller:", + "MASTER/CONTROLLER NODES", + "master-1", + } + + for _, expected := range expectedStrings { + if !strings.Contains(result, expected) { + t.Errorf("renderNodesView() should contain %q", expected) + } + } +} + +func TestApp_RenderNodesView_DataNodes(t *testing.T) { + app := &App{ + nodes: []NodeInfo{ + { + Name: "data-1", + NodeRole: "d", + Master: "-", + HeapPercent: "70", + CPU: "50", + RAMPercent: "80", + DiskUsedPercent: "60", + DiskUsed: "100GB", + DiskTotal: "250GB", + }, + }, + } + + result := app.renderNodesView() + + expectedStrings := []string{ + "Nodes (1)", + "Data Nodes:", + "DATA NODES", + "data-1", + } + + for _, expected := range expectedStrings { + if !strings.Contains(result, expected) { + t.Errorf("renderNodesView() should contain %q", expected) + } + } +} + +func TestApp_RenderNodesView_MixedNodes(t *testing.T) { + app := &App{ + nodes: []NodeInfo{ + { + Name: "master-1", + NodeRole: "m", + Master: "*", + HeapPercent: "50", + CPU: "30", + RAMPercent: "60", + DiskUsedPercent: "40", + }, + { + Name: "data-1", + NodeRole: "md", + Master: "-", + HeapPercent: "70", + CPU: "50", + RAMPercent: "80", + DiskUsedPercent: "60", + }, + { + Name: "ingest-1", + NodeRole: "i", + Master: "-", + HeapPercent: "40", + CPU: "20", + RAMPercent: "50", + DiskUsedPercent: "30", + }, + }, + } + + result := app.renderNodesView() + + if !strings.Contains(result, "Nodes (3)") { + t.Error("renderNodesView() should show total node count") + } + + if !strings.Contains(result, "Other:") { + t.Error("renderNodesView() should show 'Other' section when there are non-master/data nodes") + } + + if !strings.Contains(result, "OTHER NODES") { + t.Error("renderNodesView() should have OTHER NODES section") + } +} + +func TestApp_RenderNode_WithDiskInfo(t *testing.T) { + app := &App{} + var b strings.Builder + + node := NodeInfo{ + Name: "test-node", + NodeRole: "md", + Master: "-", + HeapPercent: "75", + CPU: "60", + RAMPercent: "85", + DiskUsedPercent: "70", + DiskUsed: "700GB", + DiskTotal: "1TB", + } + + app.renderNode(&b, node) + result := b.String() + + expectedStrings := []string{ + "test-node", + "Heap:", + "CPU:", + "RAM:", + "Disk:", + "700GB", + "1TB", + } + + for _, expected := range expectedStrings { + if !strings.Contains(result, expected) { + t.Errorf("renderNode() should contain %q", expected) + } + } +} + +func TestApp_RenderNode_ActiveMaster(t *testing.T) { + app := &App{} + var b strings.Builder + + node := NodeInfo{ + Name: "master-node", + NodeRole: "m", + Master: "*", + HeapPercent: "50", + CPU: "30", + RAMPercent: "60", + DiskUsedPercent: "40", + } + + app.renderNode(&b, node) + result := b.String() + + if !strings.Contains(result, "ACTIVE MASTER") { + t.Error("renderNode() should show ACTIVE MASTER for master nodes") + } + + if !strings.Contains(result, "master-node") { + t.Error("renderNode() should contain node name") + } +} + +func TestApp_RenderNode_NoDiskInfo(t *testing.T) { + app := &App{} + var b strings.Builder + + node := NodeInfo{ + Name: "test-node", + NodeRole: "m", + Master: "-", + HeapPercent: "50", + CPU: "30", + RAMPercent: "60", + DiskUsedPercent: "", + } + + app.renderNode(&b, node) + result := b.String() + + // Disk line should not be present when DiskUsedPercent is empty + if strings.Contains(result, "Disk:") { + t.Error("renderNode() should not show Disk info when DiskUsedPercent is empty") + } +} diff --git a/internal/ui/views_resources_test.go b/internal/ui/views_resources_test.go index f68227d..d645cf1 100644 --- a/internal/ui/views_resources_test.go +++ b/internal/ui/views_resources_test.go @@ -152,3 +152,246 @@ func TestApp_RenderFields_Indentation(t *testing.T) { }) } } + +func TestApp_CountFields_SimpleFields(t *testing.T) { + app := &App{} + + properties := map[string]interface{}{ + "field1": map[string]interface{}{ + "type": "text", + }, + "field2": map[string]interface{}{ + "type": "keyword", + }, + "field3": map[string]interface{}{ + "type": "integer", + }, + } + + count := app.countFields(properties) + expected := 3 + + if count != expected { + t.Errorf("countFields() = %d, want %d", count, expected) + } +} + +func TestApp_CountFields_NestedFields(t *testing.T) { + app := &App{} + + properties := map[string]interface{}{ + "user": map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "name": map[string]interface{}{ + "type": "text", + }, + "email": map[string]interface{}{ + "type": "keyword", + }, + }, + }, + } + + count := app.countFields(properties) + // Should count "user" + "name" + "email" = 3 + expected := 3 + + if count != expected { + t.Errorf("countFields() with nested fields = %d, want %d", count, expected) + } +} + +func TestApp_CountFields_DeepNesting(t *testing.T) { + app := &App{} + + properties := map[string]interface{}{ + "level1": map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "level2": map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "level3": map[string]interface{}{ + "type": "text", + }, + }, + }, + }, + }, + } + + count := app.countFields(properties) + // Should count all levels: level1 + level2 + level3 = 3 + expected := 3 + + if count != expected { + t.Errorf("countFields() with deep nesting = %d, want %d", count, expected) + } +} + +func TestApp_CountFields_EmptyProperties(t *testing.T) { + app := &App{} + + properties := map[string]interface{}{} + + count := app.countFields(properties) + expected := 0 + + if count != expected { + t.Errorf("countFields() with empty properties = %d, want %d", count, expected) + } +} + +func TestApp_FormatFieldType(t *testing.T) { + app := &App{} + + tests := []struct { + name string + fieldType string + }{ + {"text", "text"}, + {"keyword", "keyword"}, + {"long", "long"}, + {"integer", "integer"}, + {"short", "short"}, + {"byte", "byte"}, + {"double", "double"}, + {"float", "float"}, + {"half_float", "half_float"}, + {"scaled_float", "scaled_float"}, + {"date", "date"}, + {"boolean", "boolean"}, + {"object", "object"}, + {"nested", "nested"}, + {"unknown_type", "unknown_type"}, + {"binary", "binary"}, + {"geo_point", "geo_point"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := app.formatFieldType(tt.fieldType) + // Just verify it returns a non-empty string + // The actual formatting includes ANSI codes which are hard to test + if result == "" { + t.Errorf("formatFieldType(%q) returned empty string", tt.fieldType) + } + }) + } +} + +func TestApp_RenderResourcesView_NoNodes(t *testing.T) { + app := &App{ + nodes: []NodeInfo{}, + } + + result := app.renderResourcesView() + + if !strings.Contains(result, "No node data available") { + t.Error("renderResourcesView() with no nodes should display 'No node data available'") + } +} + +func TestApp_RenderResourcesView_WithNodes(t *testing.T) { + app := &App{ + nodes: []NodeInfo{ + { + Name: "node1", + HeapPercent: "50", + CPU: "30", + RAMPercent: "60", + DiskUsedPercent: "40", + }, + { + Name: "node2", + HeapPercent: "70", + CPU: "50", + RAMPercent: "80", + DiskUsedPercent: "60", + }, + }, + } + + result := app.renderResourcesView() + + expectedStrings := []string{ + "Resource Utilization Dashboard", + "Cluster Averages", + "JVM Heap:", + "CPU:", + "RAM:", + "Disk:", + } + + for _, expected := range expectedStrings { + if !strings.Contains(result, expected) { + t.Errorf("renderResourcesView() should contain %q", expected) + } + } +} + +func TestApp_RenderIndexSchemaView_NoMapping(t *testing.T) { + app := &App{ + selectedIndexName: "test-index", + indexMapping: nil, + } + + result := app.renderIndexSchemaView() + + if !strings.Contains(result, "test-index") { + t.Error("renderIndexSchemaView() should contain index name") + } + + if !strings.Contains(result, "Loading mapping...") { + t.Error("renderIndexSchemaView() with nil mapping should show 'Loading mapping...'") + } +} + +func TestApp_RenderIndexSchemaView_NoProperties(t *testing.T) { + app := &App{ + selectedIndexName: "test-index", + indexMapping: &IndexMapping{ + Mappings: map[string]interface{}{}, + }, + } + + result := app.renderIndexSchemaView() + + if !strings.Contains(result, "No properties found in mapping") { + t.Error("renderIndexSchemaView() without properties should show error message") + } +} + +func TestApp_RenderIndexSchemaView_WithProperties(t *testing.T) { + app := &App{ + selectedIndexName: "test-index", + indexMapping: &IndexMapping{ + Mappings: map[string]interface{}{ + "properties": map[string]interface{}{ + "title": map[string]interface{}{ + "type": "text", + }, + "status": map[string]interface{}{ + "type": "keyword", + }, + }, + }, + }, + } + + result := app.renderIndexSchemaView() + + expectedStrings := []string{ + "test-index", + "Fields (2)", + "title", + "status", + } + + for _, expected := range expectedStrings { + if !strings.Contains(result, expected) { + t.Errorf("renderIndexSchemaView() should contain %q", expected) + } + } +}