diff options
Diffstat (limited to 'internal/html_test.go')
| -rw-r--r-- | internal/html_test.go | 496 |
1 files changed, 496 insertions, 0 deletions
diff --git a/internal/html_test.go b/internal/html_test.go new file mode 100644 index 0000000..b22eea4 --- /dev/null +++ b/internal/html_test.go @@ -0,0 +1,496 @@ +package internal + +import ( + "os" + "path/filepath" + "strings" + "testing" + "time" +) + +// TestHtmlStatusBadge tests the htmlStatusBadge function. +func TestHtmlStatusBadge(t *testing.T) { + tests := []struct { + status nagiosCode + expected string + }{ + {nagiosOk, `<span class="OK">OK</span>`}, + {nagiosWarning, `<span class="WARNING">WARNING</span>`}, + {nagiosCritical, `<span class="CRITICAL">CRITICAL</span>`}, + {nagiosUnknown, `<span class="UNKNOWN">UNKNOWN</span>`}, + } + + for _, tt := range tests { + result := htmlStatusBadge(tt.status) + if result != tt.expected { + t.Errorf("htmlStatusBadge(%v) = %q, want %q", tt.status, result, tt.expected) + } + } +} + +// TestHtmlHeader tests the htmlHeader function. +func TestHtmlHeader(t *testing.T) { + subject := "GOGIOS Report [C:1 W:2 U:3 S:4 OK:5]" + result := htmlHeader(subject, 1, 2, 3, 4, 5) + + // Check that the header contains expected elements + expectedElements := []string{ + "<!DOCTYPE html>", + `<html lang="en">`, + "<head>", + `<meta charset="UTF-8">`, + `<meta name="viewport"`, + "<title>", + subject, + `<meta http-equiv="refresh" content="300">`, + "<style>", + ".CRITICAL { color: #dc3545; }", + ".WARNING { color: #ff8c00; }", + "Gogios Status Report", + "C:1 W:2 U:3 S:4 OK:5", + "Last Updated:", + } + + for _, elem := range expectedElements { + if !strings.Contains(result, elem) { + t.Errorf("htmlHeader() missing expected element: %q", elem) + } + } +} + +// TestHtmlFooter tests the htmlFooter function. +func TestHtmlFooter(t *testing.T) { + result := htmlFooter() + + expectedElements := []string{ + "Generated by Gogios at", + "</div>", + "</body>", + "</html>", + } + + for _, elem := range expectedElements { + if !strings.Contains(result, elem) { + t.Errorf("htmlFooter() missing expected element: %q", elem) + } + } +} + +// TestHtmlReportBy tests the htmlReportBy method with various filters. +func TestHtmlReportBy(t *testing.T) { + now := time.Now().Unix() + staleEpoch := now - 7200 // 2 hours ago + + s := state{ + staleEpoch: staleEpoch, + checks: map[string]checkState{ + "check1": { + Status: nagiosCritical, + PrevStatus: nagiosOk, + Epoch: now, + output: "Service is down", + }, + "check2": { + Status: nagiosWarning, + PrevStatus: nagiosWarning, + Epoch: now, + output: "High load", + }, + "check3": { + Status: nagiosOk, + PrevStatus: nagiosOk, + Epoch: staleEpoch - 100, // stale + output: "All good", + }, + }, + } + + // Test critical filter + var sb strings.Builder + count := s.htmlReportBy(&sb, false, false, func(cs checkState) bool { + return cs.Status == nagiosCritical + }) + + if count != 1 { + t.Errorf("htmlReportBy(critical filter) count = %d, want 1", count) + } + + result := sb.String() + if !strings.Contains(result, "check1") { + t.Error("htmlReportBy(critical filter) missing check1") + } + if !strings.Contains(result, "Service is down") { + t.Error("htmlReportBy(critical filter) missing output text") + } + if !strings.Contains(result, "CRITICAL") { + t.Error("htmlReportBy(critical filter) missing CRITICAL status") + } + + // Test status change filter + sb.Reset() + count = s.htmlReportBy(&sb, true, false, func(cs checkState) bool { + return cs.Status == nagiosCritical && cs.changed() + }) + + if count != 1 { + t.Errorf("htmlReportBy(changed filter) count = %d, want 1", count) + } + + result = sb.String() + if !strings.Contains(result, "→") { + t.Error("htmlReportBy(changed filter) missing status change arrow") + } + if !strings.Contains(result, "OK") { + t.Error("htmlReportBy(changed filter) missing previous OK status") + } +} + +// TestHtmlReportChanged tests the htmlReportChanged method. +func TestHtmlReportChanged(t *testing.T) { + now := time.Now().Unix() + + s := state{ + staleEpoch: now - 3600, + checks: map[string]checkState{ + "check1": { + Status: nagiosCritical, + PrevStatus: nagiosOk, + Epoch: now, + output: "Service failed", + }, + "check2": { + Status: nagiosOk, + PrevStatus: nagiosOk, + Epoch: now, + output: "Still OK", + }, + }, + } + + var sb strings.Builder + changed := s.htmlReportChanged(&sb) + + if !changed { + t.Error("htmlReportChanged() returned false, want true") + } + + result := sb.String() + if !strings.Contains(result, "check1") { + t.Error("htmlReportChanged() missing changed check") + } + if strings.Contains(result, "check2") { + t.Error("htmlReportChanged() should not include unchanged check") + } +} + +// TestHtmlReport tests the complete HTML report generation. +func TestHtmlReport(t *testing.T) { + now := time.Now().Unix() + staleEpoch := now - 3600 + + s := state{ + staleEpoch: staleEpoch, + checks: map[string]checkState{ + "critical_check": { + Status: nagiosCritical, + PrevStatus: nagiosOk, + Epoch: now, + output: "Service is down", + }, + "warning_check": { + Status: nagiosWarning, + PrevStatus: nagiosWarning, + Epoch: now, + output: "High CPU usage", + }, + "ok_check": { + Status: nagiosOk, + PrevStatus: nagiosOk, + Epoch: now, + output: "Everything fine", + }, + "stale_check": { + Status: nagiosOk, + PrevStatus: nagiosOk, + Epoch: staleEpoch - 100, + output: "Not updated recently", + }, + }, + } + + subject := "GOGIOS Report [C:1 W:1 U:0 S:1 OK:2]" + result := s.htmlReport(subject) + + // Check that all major sections are present + expectedSections := []string{ + "<!DOCTYPE html>", + "Gogios Status Report", + "Alerts with status changed", + "Unhandled alerts", + "Stale alerts", + "Generated by Gogios", + "</html>", + } + + for _, section := range expectedSections { + if !strings.Contains(result, section) { + t.Errorf("htmlReport() missing section: %q", section) + } + } + + // Check that check names appear + if !strings.Contains(result, "critical_check") { + t.Error("htmlReport() missing critical_check") + } + if !strings.Contains(result, "warning_check") { + t.Error("htmlReport() missing warning_check") + } + + // Check status summary + if !strings.Contains(result, "C:1 W:1 U:0 S:1 OK:2") { + t.Error("htmlReport() missing correct status summary") + } +} + +// TestHtmlEscaping tests that HTML special characters are properly escaped. +func TestHtmlEscaping(t *testing.T) { + now := time.Now().Unix() + + s := state{ + staleEpoch: now - 3600, + checks: map[string]checkState{ + "<script>alert('xss')</script>": { + Status: nagiosCritical, + PrevStatus: nagiosOk, + Epoch: now, + output: "Output with <tags> & \"quotes\"", + }, + }, + } + + var sb strings.Builder + s.htmlReportBy(&sb, false, false, func(cs checkState) bool { + return cs.Status == nagiosCritical + }) + + result := sb.String() + + // Check that HTML is escaped + if strings.Contains(result, "<script>") { + t.Error("htmlReportBy() did not escape <script> tag in check name") + } + if !strings.Contains(result, "<script>") { + t.Error("htmlReportBy() missing escaped <script> tag") + } + if strings.Contains(result, "<tags>") && !strings.Contains(result, "<tags>") { + t.Error("htmlReportBy() did not escape <tags> in output") + } +} + +// TestPersistHTMLReport tests the file creation and atomic write. +func TestPersistHTMLReport(t *testing.T) { + // Create a temporary directory for testing + tmpDir := t.TempDir() + htmlFile := filepath.Join(tmpDir, "subdir", "status.html") + + conf := config{ + HTMLStatusFile: htmlFile, + } + + now := time.Now().Unix() + s := state{ + staleEpoch: now - 3600, + checks: map[string]checkState{ + "test_check": { + Status: nagiosCritical, + PrevStatus: nagiosOk, + Epoch: now, + output: "Test output", + }, + }, + } + + subject := "GOGIOS Report [C:1 W:0 U:0 S:0 OK:0]" + + // Test that the function creates the directory and file + err := persistHTMLReport(s, subject, conf) + if err != nil { + t.Fatalf("persistHTMLReport() error = %v", err) + } + + // Check that the file was created + if _, err := os.Stat(htmlFile); os.IsNotExist(err) { + t.Error("persistHTMLReport() did not create the HTML file") + } + + // Check that the temp file was removed + tmpFile := htmlFile + ".tmp" + if _, err := os.Stat(tmpFile); !os.IsNotExist(err) { + t.Error("persistHTMLReport() did not clean up temporary file") + } + + // Read and verify the content + content, err := os.ReadFile(htmlFile) + if err != nil { + t.Fatalf("Failed to read HTML file: %v", err) + } + + contentStr := string(content) + if !strings.Contains(contentStr, "<!DOCTYPE html>") { + t.Error("HTML file missing DOCTYPE declaration") + } + if !strings.Contains(contentStr, "test_check") { + t.Error("HTML file missing test check") + } + if !strings.Contains(contentStr, "Test output") { + t.Error("HTML file missing check output") + } + if !strings.Contains(contentStr, "C:1 W:0 U:0 S:0 OK:0") { + t.Error("HTML file missing status summary") + } +} + +// TestFederatedChecks tests HTML generation for federated checks. +func TestFederatedChecks(t *testing.T) { + now := time.Now().Unix() + + s := state{ + staleEpoch: now - 3600, + checks: map[string]checkState{ + "remote_check": { + Status: nagiosCritical, + PrevStatus: nagiosOk, + Epoch: now, + output: "Remote service down", + federatedFrom: "remote.example.com", + }, + }, + } + + var sb strings.Builder + s.htmlReportBy(&sb, false, false, func(cs checkState) bool { + return cs.Status == nagiosCritical + }) + + result := sb.String() + + if !strings.Contains(result, "federated from") { + t.Error("htmlReportBy() missing federated indicator") + } + if !strings.Contains(result, "remote.example.com") { + t.Error("htmlReportBy() missing federated hostname") + } +} + +// TestW3CCompliance tests that generated HTML meets W3C HTML5 standards. +func TestW3CCompliance(t *testing.T) { + now := time.Now().Unix() + + s := state{ + staleEpoch: now - 3600, + checks: map[string]checkState{ + "test_check": { + Status: nagiosCritical, + PrevStatus: nagiosOk, + Epoch: now, + output: "Test output", + }, + }, + } + + subject := "GOGIOS Report [C:1 W:0 U:0 S:0 OK:0]" + html := s.htmlReport(subject) + + // W3C HTML5 Required Elements + requiredElements := map[string]string{ + "DOCTYPE": "<!DOCTYPE html>", + "html with lang": `<html lang="en">`, + "charset meta": `<meta charset="UTF-8">`, + "viewport meta": `<meta name="viewport"`, + "title": "<title>", + "closing html": "</html>", + "closing head": "</head>", + "closing body": "</body>", + "proper html close": "</html>", + } + + for name, elem := range requiredElements { + if !strings.Contains(html, elem) { + t.Errorf("W3C compliance check failed: missing %s (%q)", name, elem) + } + } + + // Check that DOCTYPE is at the very beginning + if !strings.HasPrefix(html, "<!DOCTYPE html>") { + t.Error("W3C compliance: DOCTYPE must be the first line") + } + + // Check proper tag nesting (basic validation) + htmlTagStart := strings.Index(html, "<html") + htmlTagEnd := strings.LastIndex(html, "</html>") + if htmlTagStart == -1 || htmlTagEnd == -1 { + t.Error("W3C compliance: missing <html> or </html> tag") + } + if htmlTagStart > htmlTagEnd { + t.Error("W3C compliance: </html> appears before <html>") + } + + headStart := strings.Index(html, "<head>") + headEnd := strings.Index(html, "</head>") + if headStart == -1 || headEnd == -1 { + t.Error("W3C compliance: missing <head> or </head> tag") + } + if headStart > headEnd { + t.Error("W3C compliance: </head> appears before <head>") + } + + bodyStart := strings.Index(html, "<body>") + bodyEnd := strings.LastIndex(html, "</body>") + if bodyStart == -1 || bodyEnd == -1 { + t.Error("W3C compliance: missing <body> or </body> tag") + } + if bodyStart > bodyEnd { + t.Error("W3C compliance: </body> appears before <body>") + } + + // Head should come before body + if headEnd > bodyStart { + t.Error("W3C compliance: <head> section should be closed before <body> starts") + } + + // Check for proper character encoding in meta tag + if !strings.Contains(html, `charset="UTF-8"`) { + t.Error("W3C compliance: charset should be UTF-8") + } + + // Verify no common HTML errors + commonErrors := []string{ + "<<", // Double tag opening + ">>", // Double tag closing + "< ", // Space after tag opening + " >", // Space before tag closing (in tag name) + "<//>", // Malformed closing tag + "</ >", // Empty closing tag + "< >", // Empty tag + } + + for _, err := range commonErrors { + if strings.Contains(html, err) { + t.Errorf("W3C compliance: found malformed HTML pattern: %q", err) + } + } + + // Verify all div tags are properly closed + divOpen := strings.Count(html, "<div") + divClose := strings.Count(html, "</div>") + if divOpen != divClose { + t.Errorf("W3C compliance: mismatched div tags (open: %d, close: %d)", divOpen, divClose) + } + + // Verify all span tags are properly closed + spanOpen := strings.Count(html, "<span") + spanClose := strings.Count(html, "</span>") + if spanOpen != spanClose { + t.Errorf("W3C compliance: mismatched span tags (open: %d, close: %d)", spanOpen, spanClose) + } +} |
