summaryrefslogtreecommitdiff
path: root/internal/html_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/html_test.go')
-rw-r--r--internal/html_test.go496
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, "&lt;script&gt;") {
+ t.Error("htmlReportBy() missing escaped <script> tag")
+ }
+ if strings.Contains(result, "<tags>") && !strings.Contains(result, "&lt;tags&gt;") {
+ 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)
+ }
+}