From ed3773b3248d6f4000e2d8e502d89a19f06dc438 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sun, 30 Nov 2025 01:03:05 +0000 Subject: [PATCH] Add unit tests for email template utility functions Test coverage for titleCase, formatDuration, pluralize, formatMetricValue, and formatMetricThreshold functions. 71 test cases covering edge cases, boundary conditions, and all metric type variants. --- internal/notifications/email_template_test.go | 517 ++++++++++++++++++ 1 file changed, 517 insertions(+) create mode 100644 internal/notifications/email_template_test.go diff --git a/internal/notifications/email_template_test.go b/internal/notifications/email_template_test.go new file mode 100644 index 0000000..a4ec5df --- /dev/null +++ b/internal/notifications/email_template_test.go @@ -0,0 +1,517 @@ +package notifications + +import ( + "testing" + "time" +) + +func TestTitleCase(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "empty string", + input: "", + expected: "", + }, + { + name: "single lowercase word", + input: "hello", + expected: "Hello", + }, + { + name: "single uppercase word", + input: "HELLO", + expected: "Hello", + }, + { + name: "single mixed case word", + input: "hElLo", + expected: "Hello", + }, + { + name: "multiple words lowercase", + input: "hello world", + expected: "Hello World", + }, + { + name: "multiple words uppercase", + input: "HELLO WORLD", + expected: "Hello World", + }, + { + name: "multiple words mixed case", + input: "hElLo WoRlD", + expected: "Hello World", + }, + { + name: "leading space", + input: " hello", + expected: " Hello", + }, + { + name: "trailing space", + input: "hello ", + expected: "Hello ", + }, + { + name: "multiple spaces between words", + input: "hello world", + expected: "Hello World", + }, + { + name: "tab separator", + input: "hello\tworld", + expected: "Hello\tWorld", + }, + { + name: "newline separator", + input: "hello\nworld", + expected: "Hello\nWorld", + }, + { + name: "single character", + input: "a", + expected: "A", + }, + { + name: "numbers and letters", + input: "test123 abc456", + expected: "Test123 Abc456", + }, + { + name: "hyphenated word stays joined", + input: "hello-world", + expected: "Hello-world", + }, + { + name: "underscore stays joined", + input: "hello_world", + expected: "Hello_world", + }, + { + name: "already title case", + input: "Hello World", + expected: "Hello World", + }, + { + name: "only spaces", + input: " ", + expected: " ", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := titleCase(tt.input) + if result != tt.expected { + t.Errorf("titleCase(%q) = %q, want %q", tt.input, result, tt.expected) + } + }) + } +} + +func TestFormatDuration(t *testing.T) { + tests := []struct { + name string + duration time.Duration + expected string + }{ + { + name: "zero duration", + duration: 0, + expected: "0 seconds", + }, + { + name: "one second", + duration: time.Second, + expected: "1 seconds", + }, + { + name: "30 seconds", + duration: 30 * time.Second, + expected: "30 seconds", + }, + { + name: "59 seconds", + duration: 59 * time.Second, + expected: "59 seconds", + }, + { + name: "one minute exactly", + duration: time.Minute, + expected: "1 minutes", + }, + { + name: "90 seconds", + duration: 90 * time.Second, + expected: "1 minutes", + }, + { + name: "30 minutes", + duration: 30 * time.Minute, + expected: "30 minutes", + }, + { + name: "59 minutes", + duration: 59 * time.Minute, + expected: "59 minutes", + }, + { + name: "one hour exactly", + duration: time.Hour, + expected: "1.0 hours", + }, + { + name: "1.5 hours", + duration: 90 * time.Minute, + expected: "1.5 hours", + }, + { + name: "12 hours", + duration: 12 * time.Hour, + expected: "12.0 hours", + }, + { + name: "23 hours", + duration: 23 * time.Hour, + expected: "23.0 hours", + }, + { + name: "one day exactly", + duration: 24 * time.Hour, + expected: "1.0 days", + }, + { + name: "1.5 days", + duration: 36 * time.Hour, + expected: "1.5 days", + }, + { + name: "7 days", + duration: 7 * 24 * time.Hour, + expected: "7.0 days", + }, + { + name: "30 days", + duration: 30 * 24 * time.Hour, + expected: "30.0 days", + }, + { + name: "sub-second durations truncate to 0 seconds", + duration: 500 * time.Millisecond, + expected: "0 seconds", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := formatDuration(tt.duration) + if result != tt.expected { + t.Errorf("formatDuration(%v) = %q, want %q", tt.duration, result, tt.expected) + } + }) + } +} + +func TestPluralize(t *testing.T) { + tests := []struct { + name string + count int + expected string + }{ + { + name: "count zero", + count: 0, + expected: "s", + }, + { + name: "count one", + count: 1, + expected: "", + }, + { + name: "count two", + count: 2, + expected: "s", + }, + { + name: "count ten", + count: 10, + expected: "s", + }, + { + name: "count negative", + count: -1, + expected: "s", + }, + { + name: "large count", + count: 1000000, + expected: "s", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := pluralize(tt.count) + if result != tt.expected { + t.Errorf("pluralize(%d) = %q, want %q", tt.count, result, tt.expected) + } + }) + } +} + +func TestFormatMetricValue(t *testing.T) { + tests := []struct { + name string + metricType string + value float64 + expected string + }{ + // CPU metrics + { + name: "cpu percentage", + metricType: "cpu", + value: 85.5, + expected: "85.5%", + }, + { + name: "CPU uppercase", + metricType: "CPU", + value: 95.3, + expected: "95.3%", + }, + // Memory metrics + { + name: "memory percentage", + metricType: "memory", + value: 72.8, + expected: "72.8%", + }, + { + name: "Memory mixed case", + metricType: "Memory", + value: 50.0, + expected: "50.0%", + }, + // Disk metrics + { + name: "disk percentage", + metricType: "disk", + value: 90.1, + expected: "90.1%", + }, + { + name: "usage percentage", + metricType: "usage", + value: 45.5, + expected: "45.5%", + }, + // Disk I/O metrics + { + name: "diskread rate", + metricType: "diskread", + value: 125.7, + expected: "125.7 MB/s", + }, + { + name: "diskwrite rate", + metricType: "diskwrite", + value: 50.3, + expected: "50.3 MB/s", + }, + { + name: "DiskRead uppercase", + metricType: "DiskRead", + value: 100.0, + expected: "100.0 MB/s", + }, + // Network metrics + { + name: "networkin rate", + metricType: "networkin", + value: 75.2, + expected: "75.2 MB/s", + }, + { + name: "networkout rate", + metricType: "networkout", + value: 30.8, + expected: "30.8 MB/s", + }, + // Temperature metrics + { + name: "temperature celsius", + metricType: "temperature", + value: 65.5, + expected: "65.5°C", + }, + { + name: "Temperature uppercase", + metricType: "Temperature", + value: 80.0, + expected: "80.0°C", + }, + // Unknown metrics + { + name: "unknown metric type", + metricType: "custom", + value: 123.456, + expected: "123.5", + }, + { + name: "empty metric type", + metricType: "", + value: 99.9, + expected: "99.9", + }, + // Edge cases + { + name: "zero value", + metricType: "cpu", + value: 0.0, + expected: "0.0%", + }, + { + name: "negative value", + metricType: "temperature", + value: -10.5, + expected: "-10.5°C", + }, + { + name: "large value", + metricType: "diskread", + value: 1000.0, + expected: "1000.0 MB/s", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := formatMetricValue(tt.metricType, tt.value) + if result != tt.expected { + t.Errorf("formatMetricValue(%q, %v) = %q, want %q", tt.metricType, tt.value, result, tt.expected) + } + }) + } +} + +func TestFormatMetricThreshold(t *testing.T) { + tests := []struct { + name string + metricType string + threshold float64 + expected string + }{ + // CPU metrics - note: threshold uses %.0f (no decimal places) + { + name: "cpu threshold", + metricType: "cpu", + threshold: 80.0, + expected: "80%", + }, + { + name: "CPU uppercase", + metricType: "CPU", + threshold: 90.0, + expected: "90%", + }, + // Memory metrics + { + name: "memory threshold", + metricType: "memory", + threshold: 75.0, + expected: "75%", + }, + // Disk metrics + { + name: "disk threshold", + metricType: "disk", + threshold: 85.0, + expected: "85%", + }, + { + name: "usage threshold", + metricType: "usage", + threshold: 95.0, + expected: "95%", + }, + // Disk I/O metrics + { + name: "diskread threshold", + metricType: "diskread", + threshold: 100.0, + expected: "100 MB/s", + }, + { + name: "diskwrite threshold", + metricType: "diskwrite", + threshold: 50.0, + expected: "50 MB/s", + }, + // Network metrics + { + name: "networkin threshold", + metricType: "networkin", + threshold: 200.0, + expected: "200 MB/s", + }, + { + name: "networkout threshold", + metricType: "networkout", + threshold: 150.0, + expected: "150 MB/s", + }, + // Temperature metrics + { + name: "temperature threshold", + metricType: "temperature", + threshold: 80.0, + expected: "80°C", + }, + // Unknown metrics + { + name: "unknown metric type", + metricType: "custom", + threshold: 500.0, + expected: "500", + }, + { + name: "empty metric type", + metricType: "", + threshold: 100.0, + expected: "100", + }, + // Edge cases + { + name: "zero threshold", + metricType: "cpu", + threshold: 0.0, + expected: "0%", + }, + { + name: "decimal threshold rounds", + metricType: "cpu", + threshold: 85.7, + expected: "86%", + }, + { + name: "negative threshold", + metricType: "temperature", + threshold: -20.0, + expected: "-20°C", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := formatMetricThreshold(tt.metricType, tt.threshold) + if result != tt.expected { + t.Errorf("formatMetricThreshold(%q, %v) = %q, want %q", tt.metricType, tt.threshold, result, tt.expected) + } + }) + } +}